合约的访问控制
访问控制的意思就是谁被允许做这件事。这在智能合约中非常重要。合约的访问控制可以治理谁可以铸造代币,谁可以提案,或冻结或转移或者其他权限。
所有权和Ownable
最常见和最基本的访问控制是所有权的概念:合约有一个owener的账号可以做一些管理的任务。这个方法可以完美合理的适用于只有一个管理员用户的情况。
OpenZeppelin合约提供了一个Ownable用于实现合约里面的所有权。
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
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()!
}
}
默认情况下,一个Ownable 合约的所有人是部署者的账号。
Ownable 也可以做到:
transferOwnership转移所有者账户到另一个人;
renounceOwnership所有者放弃管理权限
注意:完全移除所有者权限意味着管理员功能受保护且onlyOwner 不能再被调用。
一个合约也可以是另一个合约的所有者!通过这种方法使用组合可以对合约添加一些复杂一点的访问控制功能。
RBAC基于角色的访问控制
简单的系统或需要快速作出原型可以使用简单的ownership ,实际项目中需要不同级别的授权。RBAC在这方面非常灵活。
实际项目中,我们可以定义多个角色,每个角色可以执行不同的操作。比如moderator、 minter、或者admin等角色,而不是简单的使用onlyOwner。也可以定义规则授权一些操作给某个角色,或移除某些操作。
使用AccessControl
OpenZeppelin合约提供了AccessControl作为RBAC的实现。它的用法非常简单:对计划定义的角色创建一个role identifier用于授予或撤销权限。
下面是一个简单的在ERC20 token中使用AccessControl 定义一个minter角色的例子,这个角色允许账户创建新的代币。
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
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);
}
}
相比于Ownable,AccessControl 可以做更细粒度的权限控制。
现在对ERC20 token例子增加一个burner角色,使账户可以销毁代币,通过使用onlyRole modifier即可。
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
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 token例子中使用了一个常用于以编程方式分配角色(比如构建时)的内部函数_setupRole。但是如果我们稍后想授权minter角色给其他账户该如如何做呢?
默认情况下,一个角色的账户不能从其他账户授予或撤销它:只有hasRole 可以。为了动态授予或撤销角色,我们需要角色的管理员。
每个角色都有一个关联的管理员角色,调用grantRole 和 revokeRole函数进行授权或撤销授权。
AccessControl 包含一个特殊的角色DEFAULT_ADMIN_ROLE,作为默认的所有角色的管理员角色。这个角色的账户可以管理所有其他角色,除非使用_setRoleAdmin重新指定一个管理员角色。
在ERC20 token例子中,使用默认的管理员角色:
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
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);
}
}
不像前面的例子,没有账户被授予minter或者buiner角色。但是,因为这些角色的管理员是默认的管理员角色并且被授予msg.sender,该账户可以调用grantRole给予铸造或销毁权限,也可以调用revokeRole 移除它。
查询特权账户
因为账户可能被动态授予或撤销角色,AccessControl 使用EnumerableSet,一个Solidity的mapping类型,允许key值枚举。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是一个被提议者和执行者管理的代理。当被设置为一个智能合约的owner/admin/controller时,它能确保操作被延迟。这种延迟可以确保用户对智能合约的操作被审计或检查。
使用TimelockController
默认情况下,TimelockController已部署的地址通过时间同步获得管理员权限。这个角色授权指派到提议者、执行者或其他管理员。
第一步TimelockController配置中指派至少一个提议者和一个执行者。可以在构造时执行,也可以稍后通过任何管理员角色指定。这些角色不是独占的,意味着一个账户可以同时有两个角色。
使用AccessControl 接口和bytes32值及ADMIN_ROLE, PROPOSER_ROLE 和 EXECUTOR_ROLE常量管理每个角色。
最小延迟
最小延迟可以通过调用updateDelay函数更新。