OpenZeppelin的智慧合約庫版本3已經發布!在最新版本中,他們引入了一種全新的控制功能訪問的方法。控制某些功能的訪問許可權對於確保智慧合約的安全性至關重要,而自從以太坊虛擬機器問世以來,Solidity就是如此。熟悉OpenZeppelin的智慧合約儲存庫的開發人員知道,它已經提供了根據訪問級別限制功能的選項。最常見的是由所有者Ownable合約管理的onlyOwner模式。另一個是Openzeppelin的“roles”合約,該合約使合約可以在部署之前定義多個角色並在每個功能中設定規則,以確保msg.sender擔任正確的角色。OwnableonlyOwner模式是最常用且易於實現的訪問控制方法。它是原始的但非常有效。它假定智慧合約只有一個管理員,並允許管理員將所有權轉移到另一個地址。擴充套件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合約已被刪除。PrinciplesRoles庫對其提供的功能有所限制。作為一個庫,資料儲存必須由匯入合約控制。理想情況下,訪問控制應該抽象到某種程度,而匯入合約只需要擔心對每個函式的限制。新的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的引入是使以太坊生態系統在系統安全方面更接近行業標準的重要一步。該合約得到了行業專家的大力支援。我想這個合約很快會產生一些有趣而複雜的系統,從而進一步推動。