访问控制——即“谁被允许做这件事”——在智能合约的世界中非常重要。您的合约的访问控制可能会控制谁可以铸造代币、对提案进行投票、冻结转移以及许多其他事情。因此,了解您如何实施它至关重要,以免其他人窃取您的整个系统。

所有权和 Ownable

访问控制的最常见和基本形式是所有权的概念:有一个帐户是owner合同的帐户,可以对其执行管理任务。这种方法对于具有单个管理用户的合同是完全合理的。

OpenZeppelin Contracts 提供Ownable在您的合同中实施所有权。

/ contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

contract MyContract is Ownable {
    function normalThing() public {
        // anyone can call this normalThing()
    }

    function specialThing() public onlyOwner {
        // only the owner can call specialThing()!
    }
}

默认情况下,owner一个的Ownable合同是部署它,这通常正是你想要的帐户。

Ownable 还可以让您:

  • transferOwnership 从所有者帐户到新帐户,以及
  • renounceOwnership 对于所有者放弃这种管理特权,集中管理的初始阶段之后的常见模式已经结束。
完全删除所有者将意味着受保护的管理任务onlyOwner将不再可调用!

请注意,一份合同也可以是另一份合同的所有者!这为使用Gnosis Multisig或Gnosis Safe、Aragon DAO、ERC725/uPort身份合约或创建的完全自定义合约打开了大门。

通过这种方式,您可以使用可组合性为合同添加额外的访问控制复杂层。例如,您可以使用由项目负责人运行的 2-of-3 多重签名,而不是拥有一个普通的以太坊账户(外部拥有账户,或 EOA)作为所有者。该领域的著名项目,例如MakerDAO,使用的系统与此类似。

基于角色的访问控制

虽然所有权的简单性对于简单系统或快速原型设计很有用,但通常需要不同级别的授权。您可能希望帐户有权禁止用户使用系统,但不能创建新令牌。基于角色的访问控制 (RBAC)在这方面提供了灵活性。

本质上,我们将定义多个角色,每个角色都允许执行不同的操作集。例如,一个帐户可能具有“主持人”、“管理员”或“管理员”角色,然后您将对其进行检查,而不是简单地使用onlyOwner. 可以通过onlyRole修饰符强制执行此检查。另外,您将能够定义如何为帐户授予角色、撤销角色等的规则。

大多数软件使用基于角色的访问控制系统:一些用户是普通用户,一些可能是主管或经理,还有一些通常具有管理权限。

使用 AccessControl

OpenZeppelin Contracts 提供AccessControl实现基于角色的访问控制。它的用法很简单:对于您要定义的每个角色,您将创建一个新的角色标识符,用于授予、撤销和检查帐户是否具有该角色。

这是一个AccessControlERC20令牌中使用来定义“铸币者”角色的简单示例,该角色允许拥有它的帐户创建新令牌:

/ contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20, AccessControl {
    // Create a new role identifier for the minter role
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor(address minter) ERC20("MyToken", "TKN") {
        // Grant the minter role to a specified account
        _setupRole(MINTER_ROLE, minter);
    }

    function mint(address to, uint256 amount) public {
        // Check that the calling account has the minter role
        require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
        _mint(to, amount);
    }
}
AccessControl在您的系统上使用它之前, 请确保您完全了解它的工作原理,或者复制粘贴本指南中的示例。

虽然清晰明确,但这并不是我们无法通过Ownable. 事实上,在AccessControl需要细化权限的场景中,可以通过定义多个角色来实现。

让我们通过定义一个“燃烧器”角色来扩展我们的 ERC20 令牌示例,该角色允许帐户销毁令牌,并使用onlyRole修饰符:

// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor(address minter, address burner) ERC20("MyToken", "TKN") {
        _setupRole(MINTER_ROLE, minter);
        _setupRole(BURNER_ROLE, burner);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
        _burn(from, amount);
    }
}

好干净!通过以这种方式拆分关注点,与使用更简单的所有权访问控制方法相比,可以实现更细粒度的权限级别。限制系统的每个组件能够执行的操作被称为最小权限原则,是一种很好的安全实践。请注意,如果需要,每个帐户可能仍具有多个角色。

授予和撤销角色

上面的 ERC20 令牌示例使用_setupRole,该internal函数在以编程方式分配角色时(例如在构建期间)很有用。但是,如果我们稍后想将“铸币者”角色授予其他帐户怎么办?

默认情况下,具有角色的帐户不能从其他帐户授予或撤销它:拥有角色所做的只是让hasRole检查通过。要动态授予和撤销角色,您需要角色 admin 的帮助。

每个角色都有一个关联的管理员角色,该角色授予调用grantRolerevokeRole函数的权限。如果调用帐户具有相应的管理员角色,则可以使用这些来授予或撤销角色。多个角色可能具有相同的管理员角色以简化管理。角色的管理员甚至可以是相同的角色本身,这将导致具有该角色的帐户也能够授予和撤销它。

这种机制可用于创建类似于组织结构图的复杂许可结构,但它也提供了一种管理更简单应用程序的简单方法。AccessControl包括一个名为 的特殊角色,DEFAULT_ADMIN_ROLE它充当所有角色默认管理员角色。具有此角色的帐户将能够管理任何其他角色,除非_setRoleAdmin用于选择新的管理员角色。

让我们看一下 ERC20 令牌示例,这次利用默认管理员角色:

// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor() ERC20("MyToken", "TKN") {
        // Grant the contract deployer the default admin role: it will be able
        // to grant and revoke any roles
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
        _burn(from, amount);
    }
}

请注意,与前面的示例不同,没有帐户被授予“铸币者”或“燃烧者”角色。但是,由于这些角色的 admin 角色是默认的 admin 角色,并且角色被授予msg.sender,因此同一个帐户可以调用grantRole以授予铸造或刻录权限,并revokeRole可以将其删除。

动态角色分配通常是理想的属性,例如在对参与者的信任可能随时间变化的系统中。它还可用于支持诸如KYC 之类的用例,其中角色承担者列表可能无法预先知道,或者包含在单个事务中的成本可能过高。

查询特权账户

由于帐户可能会动态授予和撤销角色,因此并不总是可以确定哪些帐户拥有特定的角色。这很重要,因为它允许证明系统的某些属性,例如管理帐户是多重签名或 DAO,或者某个角色已从所有用户中删除,从而有效地禁用任何相关功能。

在幕后,AccessControl使用EnumerableSet,这是 Soliditymapping类型的更强大的变体,它允许键枚举。getRoleMemberCount可用于检索具有特定角色的帐户数量,getRoleMember然后可以调用以获取每个帐户的地址。

const minterCount = await myToken.getRoleMemberCount(MINTER_ROLE);

const members = [];
for (let i = 0; i < minterCount; ++i) {
    members.push(await myToken.getRoleMember(MINTER_ROLE, i));
}

延迟操作

访问控制对于防止对关键功能的未授权访问至关重要。这些功能可用于铸造代币、冻结转移或执行完全改变智能合约逻辑的升级。虽然Ownable并且AccessControl可以防止未经授权的访问,但它们没有解决行为不端的管理员攻击他们自己的系统而损害其用户的问题。

这是TimelockController正在解决的问题。

TimelockController是由提议者和执行者管理的代理。当设置为智能合约的所有者/管理员/控制者时,它确保提议者订购的任何维护操作都会受到延迟。这种延迟可以保护智能合约的用户,让他们有时间审查维护操作并在他们认为这样做符合他们的最佳利益时退出系统。

使用 TimelockController

默认情况下,部署该地址的地址TimelockController在时间锁上获得管理权限。此角色授予分配提议者、执行者和其他管理员的权利。

配置的第一步TimelockController是至少分配一个提议者和一个执行者。这些可以在构建期间或以后由具有管理员角色的任何人分配。这些角色不是独占的,这意味着一个帐户可以同时拥有这两个角色。

角色使用AccessControl接口进行管理bytes32,每个角色的值都可以通过ADMIN_ROLEPROPOSER_ROLEEXECUTOR_ROLE常量访问。

在此之上还有一个附加功能AccessControl:赋予执行者角色,address(0)一旦时间锁定到期,任何人都可以访问以执行提案。此功能虽然有用,但应谨慎使用。

此时,在分配了提议者和执行者的情况下,时间锁可以执行操作。

一个可选的下一步是部署者放弃其管理权限并让时间锁自行管理。如果部署者决定这样做,所有进一步的维护,包括分配新的提议者/调度者或更改时间锁定持续时间都必须遵循时间锁定工作流程。这将时间锁的治理与附加到时间锁的合同的治理联系起来,并强制延迟时间锁维护操作。

如果部署者放弃管理权限以支持时间锁定本身,则分配新的提议者或执行者将需要时间锁定操作。这意味着,如果负责这两个角色中的任何一个的账户变得不可用,那么整个合约(及其控制的任何合约)将被无限期锁定。

分配了提议者和执行者角色并且 timelock 负责自己的管理后,您现在可以将任何合约的所有权/控制权转移到 timelock。

推荐的配置是将这两个角色授予安全治理合同,例如 DAO 或多重签名,并将执行者角色授予负责帮助维护操作的人员持有的一些 EOA。这些钱包不能接管时间锁的控制,但它们可以帮助简化工作流程。

最小延迟

由 执行的操作TimelockController不受固定延迟而是最小延迟的影响。一些重大更新可能需要更长的延迟。例如,如果仅仅几天的延迟可能足以让用户审核铸造操作,那么在安排智能合约升级时使用几周甚至几个月的延迟是有意义的。

getMinDelay可以通过调用该updateDelay函数来更新最小延迟(可通过该方法访问)。请记住,只能通过时间锁本身访问此功能,这意味着此维护操作必须通过时间锁本身。

发表评论

后才能评论