1. 链一财经首页
  2. 资讯

如何使用OpenZeppelin的新AccessControl合约

OpenZeppelin的智能合约库版本3已经发布!在最新版本中,他们引入了一种全新的控制功能访问的方法。
控制某些功能的访问权限对于确保智能合约的安全性至关重要,而自从以太坊虚拟机问世以来,Solidity就是如此。
熟悉OpenZeppelin的智能合约存储库的开发人员知道,它已经提供了根据访问级别限制功能的选项。
最常见的是由所有者Ownable合约管理的onlyOwner模式。另一个是Openzeppelin的“roles”合约,该合约使合约可以在部署之前定义多个角色并在每个功能中设置规则,以确保msg.sender担任正确的角色。
Ownable
onlyOwner模式是最常用且易于实现的访问控制方法。它是原始的但非常有效。
它假定智能合约只有一个管理员,并允许管理员将所有权转移到另一个地址。
扩展Ownable合约允许子合约使用onlyOwner自定义修饰符定义功能。这些功能要求事务的发送者是单一管理员。
function normalFunction() public {
    // anyone can call this
}
function restrictedFunction() public onlyOwner {
    // only the owner can call this
}
这是一个简单的示例,说明如何利用Ownable合约提供的自定义修饰符来限制功能访问。
#### Roles
尽管Ownable合约很受欢迎且易于使用,但存储库中的其他OpenZeppelin合约仅使用Roles库进行访问控制。这是因为Roles库在Ownable合约的刚性方面提供了灵活性。
作为一个库,它不会由子合约扩展,而是通过using语句用作为数据类型添加功能的工具。Roles库为它定义的role数据类型提供了三个功能。
代码显示了Roles的定义。
pragma solidity ^0.5.0;
/**
 * @title Roles
 * @dev Library for managing addresses assigned to a Role.
 */
library Roles {
    struct Role {
        mapping (address => bool) bearer;
    }
    /**
     * @dev Give an account access to this role.
     */
    function add(Role storage role, address account) internal {
        require(!has(role, account), “Roles: account already has role”);
        role.bearer[account] = true;
    }
    /**
     * @dev Remove an account’s access to this role.
     */
    function remove(Role storage role, address account) internal {
        require(has(role, account), “Roles: account does not have role”);
        role.bearer[account] = false;
    }
    /**
     * @dev Check if an account has this role.
     * @return bool
     */
    function has(Role storage role, address account) internal view returns (bool) {
        require(account != address(0), “Roles: account is the zero address”);
        return role.bearer[account];
    }
}
在顶部,您可以看到Role结构。合约使用它来定义多个角色及其成员。函数add(),remove()和has()是库用于与Role结构交互的函数。
例如下段代码展示了代币如何使用两个单独的角色_minters和_burners来将访问限制应用于某些功能。
pragma solidity ^0.5.0;
import “@openzeppelin/contracts/access/Roles.sol”;
import “@openzeppelin/contracts/token/ERC20/ERC20.sol”;
import “@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol”;
contract MyToken is ERC20, ERC20Detailed {
    using Roles for Roles.Role;
    Roles.Role private _minters;
    Roles.Role private _burners;
    constructor(address[] memory minters, address[] memory burners)
        ERC20Detailed(“MyToken”, “MTKN”, 18)
        public
    {
        for (uint256 i = 0; i < minters.length; ++i) {
            _minters.add(minters[i]);
        }
        for (uint256 i = 0; i < burners.length; ++i) {
            _burners.add(burners[i]);
        }
    }
    function mint(address to, uint256 amount) public {
        // Only minters can mint
        require(_minters.has(msg.sender), “DOES_NOT_HAVE_MINTER_ROLE”);
        _mint(to, amount);
    }
    function burn(address from, uint256 amount) public {
        // Only burners can burn
        require(_burners.has(msg.sender), “DOES_NOT_HAVE_BURNER_ROLE”);
       _burn(from, amount);
    }
}
注意,在mint()函数中,require语句如何通过使用_minters.has(msg.sender)函数来确保消息的发件人是一个铸造者。
考虑到这已经是一段时间的标准,对于开发人员来说,最大的新闻是从2.5.x版本升级到3.x版本后,Roles合约已被删除。
Principles
Roles库对其提供的功能有所限制。
作为一个库,数据存储必须由导入合约控制。理想情况下,访问控制应该抽象到某种程度,而导入合约只需要担心对每个函数的限制。
新的AccessControl合同被吹捧为:
一站式服务,满足所有授权需求。它使您可以轻松定义具有不同权限的多个角色,以及允许哪些帐户授予和撤消每个角色。通过启用系统中所有特权帐户的枚举,还可以提高透明度。
该语句的最后两点对于Roles库是不可能的。
OpenZeppelin看起来正朝着一种使人们联想到传统计算安全性中突出的基于角色的访问控制(RBAC)和基于属性的访问控制(ABAC)标准的系统发展。
代码解析
展示AccessControl合约代码。
pragma solidity ^0.6.0;
import “../utils/EnumerableSet.sol”;
import “../utils/Address.sol”;
import “../GSN/Context.sol”;
/**
 * @dev Contract module that allows children to implement role-based access
 * control mechanisms.
 *
 * Roles are referred to by their `bytes32` identifier. These should be exposed
 * in the external API and be unique. The best way to achieve this is by
 * using `public constant` hash digests:
 *
 * “`
 * bytes32 public constant MY_ROLE = keccak256(“MY_ROLE”);
 * “`
 *
 * Roles can be used to represent a set of permissions. To restrict access to a
 * function call, use {hasRole}:
 *
 * “`
 * function foo() public {
 *     require(hasRole(MY_ROLE, _msgSender()));
 *     …
 * }
 * “`
 *
 * Roles can be granted and revoked dynamically via the {grantRole} and
 * {revokeRole} functions. Each role has an associated admin role, and only
 * accounts that have a role’s admin role can call {grantRole} and {revokeRole}.
 *
 * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
 * that only accounts with this role will be able to grant or revoke other
 * roles. More complex role relationships can be created by using
 * {_setRoleAdmin}.
 */
abstract contract AccessControl is Context {
    using EnumerableSet for EnumerableSet.AddressSet;
    using Address for address;
    struct RoleData {
        EnumerableSet.AddressSet members;
        bytes32 adminRole;
    }
    mapping (bytes32 => RoleData) private _roles;
    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
    /**
     * @dev Emitted when `account` is granted `role`.
     *
     * `sender` is the account that originated the contract call, an admin role
     * bearer except when using {_setupRole}.
     */
    event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
    /**
     * @dev Emitted when `account` is revoked `role`.
     *
     * `sender` is the account that originated the contract call:
     *   – if using `revokeRole`, it is the admin role bearer
     *   – if using `renounceRole`, it is the role bearer (i.e. `account`)
     */
    event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) public view returns (bool) {
        return _roles[role].members.contains(account);
    }
    /**
     * @dev Returns the number of accounts that have `role`. Can be used
     * together with {getRoleMember} to enumerate all bearers of a role.
     */
    function getRoleMemberCount(bytes32 role) public view returns (uint256) {
        return _roles[role].members.length();
    }
    /**
     * @dev Returns one of the accounts that have `role`. `index` must be a
     * value between 0 and {getRoleMemberCount}, non-inclusive.
     *
     * Role bearers are not sorted in any particular way, and their ordering may
     * change at any point.
     *
     * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
     * you perform all queries on the same block. See the following
     * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
     * for more information.
     */
    function getRoleMember(bytes32 role, uint256 index) public view returns (address) {
        return _roles[role].members.at(index);
    }
    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role’s admin, use {_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) public view returns (bytes32) {
        return _roles[role].adminRole;
    }
    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * – the caller must have “role“’s admin role.
     */
    function grantRole(bytes32 role, address account) public virtual {
        require(hasRole(_roles[role].adminRole, _msgSender()), “AccessControl: sender must be an admin to grant”);
        _grantRole(role, account);
    }
    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * – the caller must have “role“’s admin role.
     */
    function revokeRole(bytes32 role, address account) public virtual {
        require(hasRole(_roles[role].adminRole, _msgSender()), “AccessControl: sender must be an admin to revoke”);
        _revokeRole(role, account);
    }
    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function’s
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been granted `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * – the caller must be `account`.
     */
    function renounceRole(bytes32 role, address account) public virtual {
        require(account == _msgSender(), “AccessControl: can only renounce roles for self”);
        _revokeRole(role, account);
    }
    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event. Note that unlike {grantRole}, this function doesn’t perform any
     * checks on the calling account.
     *
     * [WARNING]
     * ====
     * This function should only be called from the constructor when setting
     * up the initial roles for the system.
     *
     * Using this function in any other way is effectively circumventing the admin
     * system imposed by {AccessControl}.
     * ====
     */
    function _setupRole(bytes32 role, address account) internal virtual {
        _grantRole(role, account);
    }
    /**
     * @dev Sets `adminRole` as “role“’s admin role.
     */
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        _roles[role].adminRole = adminRole;
    }
    function _grantRole(bytes32 role, address account) private {
        if (_roles[role].members.add(account)) {
            emit RoleGranted(role, account, _msgSender());
        }
    }
    function _revokeRole(bytes32 role, address account) private {
        if (_roles[role].members.remove(account)) {
            emit RoleRevoked(role, account, _msgSender());
        }
    }
}
第42行上的Role Data结构使用EnumerableSet(也是版本3的新功能)作为存储成员的数据结构。这样可以轻松地对特权用户进行迭代。
该结构还将adminRole存储为bytes32变量。这定义了哪个角色充当特定角色的管理员(即该角色具有充当该角色的管理员,向用户授予和撤消该角色的能力)。
现在第57和66行中定义的角色被授予或撤消时,会发出事件。Roles合同仅提供三个功能:has(),add()和remove()。这些形式包括在AccessControl中以及额外的功能,例如获取角色计数,通过ID获取角色的特定成员以及放弃角色的能力。
如何使用它
第二段代码给出了使用Roles库的代币合约的示例,该合约需要两个单独的角色_minters和_burners。为了保持连续性,我们将使用相同的概念并应用AccessControl合约来做到这一点。
第四段代码显示了此实现。
pragma solidity ^0.6.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() public 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 {
        require(hasRole(MINTER_ROLE, msg.sender), “Caller is not a minter”);
        _mint(to, amount);
    }
    function burn(address from, uint256 amount) public {
        require(hasRole(BURNER_ROLE, msg.sender), “Caller is not a burner”);
        _burn(from, amount);
    }
}
那么发生了什么变化?首先每个角色不再在子合约中定义,因为它们存储在父合约中。
在子协定中,只有bytes32 ID作为常量状态变量存在(在本示例中为MINTER_ROLE和BURNER_ROLE)。
_setupRole()用于构造函数中,以设置角色的初始管理员,从而绕过AccessControl中grantRole()执行的检查(因为在构造时还没有管理员)。
此外函数不是将库函数作为数据类型的扩展名(即_minters.has(msg.sender)),而是本身具有内部函数(hasRole(MINTER_ROLE,msg.sender))。这使得子合约中的代码通常更清晰易读。
与Roles库相比,抽象出更多功能可以使子合约更容易在AccessControl合约之上构建。
结  论
AccessControl的引入是使以太坊生态系统在系统安全方面更接近行业标准的重要一步。
该合约得到了行业专家的大力支持。我想这个合约很快会产生一些有趣而复杂的系统,从而进一步推动。

根据国家《关于防范代币发行融资风险的公告》,大家应警惕代币发行融资与交易的风险隐患。

本文来自LIANYI转载,不代表链一财经立场,转载请联系原作者。

发表评论

登录后才能评论