Appearance
访问控制
访问控制顾名思义就是“谁可以做这件事”,这在智能合约世界中是非常重要的。智能合约中的访问控制可以规定谁可以mint tokens
,谁可以给提案投票,冻结转账等类似的事情。因此了解如何实现至关重要,以免其他人窃取你的整个系统。
所有权和Ownable
最基础和常见的访问控制就是所有权的概念:智能合约会有一个账户,它是这个合约的owner
,并且可以对其执行管理任务。这个方式对于合约这种单一管理员来说是非常合理的。
OpenZeppelin合约提供了Ownerable来实现合约中的所有权管理。
solidity
// 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()!
}
}
通常情况下,部署合约的账号就是合约的所有者。 Ownerable
标签允许你:
- transferOwnership将合约所有权转移给另外一个账户
- renounceOwnership放弃管理权,这是中心化管理结束了初始模式后的常见模式。
警告
完全删除所有者,也就意味着标记onlyOwner
标签的函数将不可调用
请注意,合约也可以是另一个合约的拥有者!这打开了使用的大门,比如Gnosis Multisig,或Gnosis Safe,Aragon DAO,一个 ERC725/uPort身份认证合约,或者是你自己完全自定义的合约。 通过这种方式,你可以在合约中添加更复杂的访问控制层。替代单一的以太坊账户,你可以使用2/3多重签名方式来主导项目运行,例如著名的MarkerDAO就是使用了类似的系统。
基于角色的访问控制
虽然所有权方案对于简单系统或者快速构建原型很有效,但通常情况下,多层级的访问控制也是需要的。你可能需要一个账户来禁止用户使用系统,但是又不想让它创建新的token。基于角色的访问控制在这方面提供了灵活性。
实际上,我们会定义多个角色,每个角色对应一组操作权限。举例来说,某个账户拥有'moderator','miner'或'admin'角色,这样你就替换onlyOwner
来进行权限查验了。这个检查可以通过onlyRole
修饰符来进行。另外,你还可以定义如何向账户授权,撤销权限等规则。
多数软件都采用基于角色的访问控制:一些用户是普通用户,一些用户是主管或经理,还有少数一些是具有管理权限。
使用AccessControl
OpenZeppelin合约提供了AccessControl来实现基于角色的访问控制。它的使用非常简单:对于每个定义的角色,只需要创建一个角色标识符,用来授权,撤销和检查账户是否拥有此角色。 下面是ERC20 Token使用AccessControl
的简单例子,它定义了一个'minter'角色,这个角色允许账户创建新的token。
solidity
// 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
的亮点在于需要细粒度权限的场景,这可以通过定义多个角色来实现。
让我们通过定义一个'burner'角色来扩展ERC20 token,它使用onlyRole
修饰符来允许账户销毁token:
solidity
// 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
函数,用来以编程的方式来分配角色(比如通过构造函数)。但是如果我们之后想给某个账户授权'minter'角色应该怎么操作呢。
默认情况下,拥有角色的账户不能被其他账户授权或撤销授权:拥有角色的判断就是通过hasRole
检查。动态授权或撤销授权,需要管理员角色。
每个角色都有关联管理员角色,它通过调用grantRole
和revokeRole
函数操作权限。如果一个账户拥有管理员权限,就可以调用这些函数来授权或撤销。多个角色可以对应同一个管理员角色来简化管理。一个角色的管理员角色也可以是它本身,这就意味着该账户的角色也能够授权或撤销。
这套机制可以用来构建类似组织结构图一样的复杂权限系统,同时它也提供了一个简单方式来管理简单系统。AccessControl
包含一个特殊的角色,叫DEFAULT_ADMIN_ROLE
,它是所有角色的默认管理员。拥有这个角色的账户可以管理其他角色,除非手工调用_setRoleAdmin
函数来指定一个新的管理员。
我们再来继续看一下ERC20的例子,这次我们利用默认管理员角色:
solidity
// 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);
}
}
请注意,和之前的例子不同,没有账户被授予'minter'和'burner'角色。然而,这两个角色拥有同一个admin角色,它被授予给了msg.sender
,这样这个账户就可以调用grantRole
来授权挖矿或销毁操作,或者调用revokeRole
来撤销授权。
动态的角色分配通常是合理的做法,比如在对参与者的信任随着时间变化的系统。它还可以使用在KYC这样的系统中,也就是角色担任者开始时并不知道,或者包含在同一个交易中,燃料费会非常高昂。
查询账户权限
由于账户可能会被动态的授权或撤销,因此我们不可能一直清楚的了解哪些账户有哪些角色权限。这一点非常重要,因为他牵涉了系统属性,比如管理员账户是使用多重签名还是DAO,或者某个角色从所有用户中删除了,从而可以有效的禁止相关功能的调用。
底层代码中,AccessControl
使用EnumerableSet
来实现关键字枚举,它是Solidity中一个非常强大的mapping
变量类型。getRoleMemberCount
可以返回某个角色中账户的数量,getRoleMember
可以返回这些账户的地址。
js
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是调用者和执行者之间的代理。当我们设置了智能合约的owner/admin/controller后,它确保了每个实际操作都会按照调用者的顺序执行,每个调用之间会有一个延迟。这个延迟保护了智能合约的用户,它们可以审查操作,并且在适当的时候退出系统。
使用TimelockController
默认情况下,部署TimelockController的账户地址获得时间锁的管理员权限。它可以给其他账户分配调用者,执行者或管理员角色。
第一步,在TimelockController中指定至少一个调用者和一个执行者。这些配置可以在构造函数里完成,或者部署后由具有管理员的用户来操作。这些角色不是互斥的,意味着一个账户可以即是调用者也可以是执行者。
这些角色是通过AccessControl接口来管理的,每个角色对应了ADMIN_ROLE
,PROPOSER_ROLE
,EXECUTOR_ROLE
这些bytes32
类型的数值常量。
有一个在AccessController
上的附加特性:将执行者角色授权给address(0)
就可以在时间锁过期后任何人都可以访问。这个特性很有用,但是使用时要非常小心。
在这里,如果调用者和执行者都已分配,那么时间锁就可以执行操作了。
下一步是可选的步骤,部署者可以放弃管理员权限,让时间锁自己来管理。如果部署者这样做了,进一步的维护,包括分配新的调用者和执行者,或者改变时间锁的持续周期将会遵循时间锁本身的流程。这将时间锁的管理和合约管理中的时间锁联系起来,并强制延迟时间锁的维护操作。
警告
如果部署者宣布放弃了管理员权限让时间锁自我管理,那么分配新的调用者和执行者将需要一个时间锁操作。这意味着如果这两个角色中的账户一旦变为不可用,那么整个合约(包括合约控制的其他合约)将会死锁。
包括调用者和执行者分配,时间锁的管理权限,你可以转移所有权/控制权给其他合约的时间锁。
提示
我们推荐的配置方式,是将角色授权等转移给一个安全的管理合约,比如DAO或多重签名,然后将调用者角色授权给一些EOAs,也就是那些帮助执行操作的账户。这些账户无法控制时间锁,但是可以让它的运行更加流畅。
最小延迟
TimelockController执行的操作不受固定延迟,而是最小延迟的影响。一些主要的升级操作可能会有一个较长时间的延迟。例如,如果审核挖矿操作需要几天的延迟,那么执行智能合约升级应该使用几周或几个月的延迟。
最小延迟(可以通过getMinDelay方法获得)可以通过updateDelay函数修改。请注意,只有时间锁本身才能访问此函数,这意味着这个维护操作必须通过时间锁本身。