訪問控制是軟體基礎設施安全的基本要素。企業應用程式需要嚴格的規則來決定誰可以做什麼,這取決於每個使用者的許可權。可以說,智慧合約中的訪問控制需要更嚴格的審查,因為漏洞可能導致惡意參與者控制系統。如今智慧合約中僅存有簡單形式的靜態訪問控制。最常見的是onlyOwner模式。另一個是Openzeppelin的“角色”智慧合約,該智慧合約可以在部署之前定義角色。儘管這為大多數智慧合約應用程式提供了良好的基礎,但基於現有的角色的訪問控制(RBAC)系統使管理員能夠在執行時動態地定義角色。從這個意義上說,Roles智慧合約是具有限制性的,因為在部署之後無法定義角色。本文介紹現有的智慧合約訪問控制模式,並提出了RBAC和基於屬性的訪問控制(ABAC)智慧合約的定義。Only OwneronlyOwner模式是智慧合約中最常用、最容易實現的訪問控制方法。程式碼1顯示了Ownable智慧合約的Openzeppelin實現。1pragma solidity ^0.5.0; 2 3import "../GSN/Context.sol"; 4/** 5 * @dev Contract module which provides a basic access control mechanism, where 6 * there is an account (an owner) that can be granted exclusive access to 7 * specific functions. 8 * 9 * This module is used through inheritance. It will make available the modifier10 * `onlyOwner`, which can be applied to your functions to restrict their use to11 * the owner.12 */13contract Ownable is Context {14 address private _owner;1516 event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);1718 /**19 * @dev Initializes the contract setting the deployer as the initial owner.20 */21 constructor () internal {22 address msgSender = _msgSender();23 _owner = msgSender;24 emit OwnershipTransferred(address(0), msgSender);25 }2627 /**28 * @dev Returns the address of the current owner.29 */30 function owner() public view returns (address) {31 return _owner;32 }3334 /**35 * @dev Throws if called by any account other than the owner.36 */37 modifier onlyOwner() {38 require(isOwner(), "Ownable: caller is not the owner");39 _;40 }4142 /**43 * @dev Returns true if the caller is the current owner.44 */45 function isOwner() public view returns (bool) {46 return _msgSender() == _owner;47 }4849 /**50 * @dev Leaves the contract without owner. It will not be possible to call51 * `onlyOwner` functions anymore. Can only be called by the current owner.52 *53 * NOTE: Renouncing ownership will leave the contract without an owner,54 * thereby removing any functionality that is only available to the owner.55 */56 function renounceOwnership() public onlyOwner {57 emit OwnershipTransferred(_owner, address(0));58 _owner = address(0);59 }6061 /**62 * @dev Transfers ownership of the contract to a new account (`newOwner`).63 * Can only be called by the current owner.64 */65 function transferOwnership(address newOwner) public onlyOwner {66 _transferOwnership(newOwner);67 }6869 /**70 * @dev Transfers ownership of the contract to a new account (`newOwner`).71 */72 function _transferOwnership(address newOwner) internal {73 require(newOwner != address(0), "Ownable: new owner is the zero address");74 emit OwnershipTransferred(_owner, newOwner);75 _owner = newOwner;76 }77}該模式假定智慧合約只有一個管理員,並使管理員可以將所有權轉移到另一個地址。擴充套件Ownable智慧合約允許子智慧合約使用onlyOwner自定義修飾符定義函式。這些函式要求事務的傳送者是單一管理員。程式碼2展示瞭如何在子智慧合約中實現此示例。1pragma solidity ^0.5.0; 2 3import "@openzeppelin/contracts/ownership/Ownable.sol"; 4 5contract MyContract is Ownable { 6 function normalThing() public { 7 // anyone can call this normalThing() 8 } 910 function specialThing() public onlyOwner {11 // only the owner can call specialThing()!12 }13}Roles如果Ownable僅限於一個管理員,則Openzeppelin的Roles庫可以定義多個角色。 1pragma solidity ^0.5.0; 2 3/** 4 * @title Roles 5 * @dev Library for managing addresses assigned to a Role. 6 */ 7library Roles { 8 struct Role { 9 mapping (address => bool) bearer;10 }1112 /**13 * @dev Give an account access to this role.14 */15 function add(Role storage role, address account) internal {16 require(!has(role, account), "Roles: account already has role");17 role.bearer[account] = true;18 }1920 /**21 * @dev Remove an account's access to this role.22 */23 function remove(Role storage role, address account) internal {24 require(has(role, account), "Roles: account does not have role");25 role.bearer[account] = false;26 }2728 /**29 * @dev Check if an account has this role.30 * @return bool31 */32 function has(Role storage role, address account) internal view returns (bool) {33 require(account != address(0), "Roles: account is the zero address");34 return role.bearer[account];35 }36}程式碼3展示了Roles智慧合約的實施。與Ownable不同,Role不提供自定義訪問修飾符。相反使用此庫的智慧合約必須在函式內部實現角色要求。他們還必須為每個角色定義Roles.Role型別狀態變數。程式碼4展示了實現兩個角色的ERC20智慧合約的示例:_burners和_minters。1pragma solidity ^0.5.0; 2 3import "@openzeppelin/contracts/access/Roles.sol"; 4import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol"; 6 7contract MyToken is ERC20, ERC20Detailed { 8 using Roles for Roles.Role; 910 Roles.Role private _minters;11 Roles.Role private _burners;1213 constructor(address[] memory minters, address[] memory burners)14 ERC20Detailed("MyToken", "MTKN", 18)15 public16 {17 for (uint256 i = 0; i < minters.length; ++i) {18 _minters.add(minters[i]);19 }2021 for (uint256 i = 0; i < burners.length; ++i) {22 _burners.add(burners[i]);23 }24 }2526 function mint(address to, uint256 amount) public {27 // Only minters can mint28 require(_minters.has(msg.sender), "DOES_NOT_HAVE_MINTER_ROLE");2930 _mint(to, amount);31 }3233 function burn(address from, uint256 amount) public {34 // Only burners can burn35 require(_burners.has(msg.sender), "DOES_NOT_HAVE_BURNER_ROLE");3637 _burn(from, amount);38 }39}它具有靈活性,允許智慧合約定義所需的儘可能多的角色,但這將require語句的實現留給了智慧合約。由於未提供自定義訪問修飾符,因此在某種程度上降低了可讀性,並且增加了引入編碼錯誤的可能性。但是沒有什麼能阻止智慧合約實現包含require語句的自定義訪問修飾符。Limitations(侷限性)這些當前的解決方案在部署智慧合約之前提供了角色的靈活性。由於角色取決於硬編碼規則,因此不支援動態建立的角色。這非常適合在已知的內部智慧合約之間提供訪問控制,但它不能提供與現代面向使用者的訪問控制軟體相當的靈活性。具有活躍使用者基礎的軟體,特別是企業軟體,本質上需要不同級別的訪問。隨著組織的發展或萎縮,訪問控制應用程式的管理員需要能夠輕鬆新增和分配新角色的能力。這些結構如今以基於角色的訪問控制(RBAC)和基於屬性的訪問控制(ABAC)的形式存在於軟體中。Role-Based Access Control (RBAC)在RBAC中,每個使用者都被分配了一個角色,每個角色都具有一組許可權,並且只要他們的角色具有正確的許可權,使用者就可以訪問資源。只要系統不需要新的角色,RBAC主要透過Roles智慧合約來滿足。Attribute-Based Access Control (ABAC)在ABAC中,為每個使用者分配了一組主題屬性,為每個資源分配了一組物件屬性。中央訪問控制機構定義有關需要執行哪些主題和物件屬性的規則。與RBAC相比,這是一個更復雜且耗時的解決方案。但是對於大型應用程式和企業而言,它更加靈活,因為它允許每個使用者獨有的各種許可權。在runtime,Ownable或Roles都不滿足這些公共模式中的任何一種。如果需要建立新角色,則需要傳送新程式碼。這些體系結構不允許InfoSec管理員透過簡單的介面建立、更新或刪除新角色。幸運的是,我們正在努力解決這一問題。Access Control (Beta)Openzeppelin即將釋出的v3.0版本(當前處於Beta版)已終止Roles庫,轉而使用名為AccessControl的abstract 智慧合約。注意:不建議您在生產應用程式中使用此解決方案,因為它仍處於測試階段。程式碼5展示了AccessControl的實現。 1pragma solidity ^0.6.0; 2 3import "../utils/EnumerableSet.sol"; 4import "../utils/Address.sol"; 5import "../GSN/Context.sol"; 6 7/** 8 * @dev Contract module that allows children to implement role-based access 9 * control mechanisms. 10 * 11 * Roles are referred to by their `bytes32` identifier. These should be exposed 12 * in the external API and be unique. The best way to achieve this is by 13 * using `public constant` hash digests: 14 * 15 * ``` 16 * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); 17 * ``` 18 * 19 * Roles can be used to represent a set of permissions. To restrict access to a 20 * function call, use {hasRole}: 21 * 22 * ``` 23 * function foo() public { 24 * require(hasRole(MY_ROLE, _msgSender())); 25 * ... 26 * } 27 * ``` 28 * 29 * Roles can be granted and revoked dynamically via the {grantRole} and 30 * {revokeRole} functions. Each role has an associated admin role, and only 31 * accounts that have a role's admin role can call {grantRole} and {revokeRole}. 32 * 33 * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means 34 * that only accounts with this role will be able to grant or revoke other 35 * roles. More complex role relationships can be created by using 36 * {_setRoleAdmin}. 37 */ 38abstract contract AccessControl is Context { 39 using EnumerableSet for EnumerableSet.AddressSet; 40 using Address for address; 41 42 struct RoleData { 43 EnumerableSet.AddressSet members; 44 bytes32 adminRole; 45 } 46 47 mapping (bytes32 => RoleData) private _roles; 48 49 bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; 50 51 /** 52 * @dev Emitted when `account` is granted `role`. 53 * 54 * `sender` is the account that originated the contract call, an admin role 55 * bearer except when using {_setupRole}. 56 */ 57 event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); 58 59 /** 60 * @dev Emitted when `account` is revoked `role`. 61 * 62 * `sender` is the account that originated the contract call: 63 * - if using `revokeRole`, it is the admin role bearer 64 * - if using `renounceRole`, it is the role bearer (i.e. `account`) 65 */ 66 event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); 67 68 /** 69 * @dev Returns `true` if `account` has been granted `role`. 70 */ 71 function hasRole(bytes32 role, address account) public view returns (bool) { 72 return _roles[role].members.contains(account); 73 } 74 75 /** 76 * @dev Returns the number of accounts that have `role`. Can be used 77 * together with {getRoleMember} to enumerate all bearers of a role. 78 */ 79 function getRoleMemberCount(bytes32 role) public view returns (uint256) { 80 return _roles[role].members.length(); 81 } 82 83 /** 84 * @dev Returns one of the accounts that have `role`. `index` must be a 85 * value between 0 and {getRoleMemberCount}, non-inclusive. 86 * 87 * Role bearers are not sorted in any particular way, and their ordering may 88 * change at any point. 89 * 90 * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure 91 * you perform all queries on the same block. See the following 92 * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] 93 * for more information. 94 */ 95 function getRoleMember(bytes32 role, uint256 index) public view returns (address) { 96 return _roles[role].members.at(index); 97 } 98 99 /**100 * @dev Returns the admin role that controls `role`. See {grantRole} and101 * {revokeRole}.102 *103 * To change a role's admin, use {_setRoleAdmin}.104 */105 function getRoleAdmin(bytes32 role) public view returns (bytes32) {106 return _roles[role].adminRole;107 }108109 /**110 * @dev Grants `role` to `account`.111 *112 * If `account` had not been already granted `role`, emits a {RoleGranted}113 * event.114 *115 * Requirements:116 *117 * - the caller must have ``role``'s admin role.118 */119 function grantRole(bytes32 role, address account) public virtual {120 require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to grant");121122 _grantRole(role, account);123 }124125 /**126 * @dev Revokes `role` from `account`.127 *128 * If `account` had been granted `role`, emits a {RoleRevoked} event.129 *130 * Requirements:131 *132 * - the caller must have ``role``'s admin role.133 */134 function revokeRole(bytes32 role, address account) public virtual {135 require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to revoke");136137 _revokeRole(role, account);138 }139140 /**141 * @dev Revokes `role` from the calling account.142 *143 * Roles are often managed via {grantRole} and {revokeRole}: this function's144 * purpose is to provide a mechanism for accounts to lose their privileges145 * if they are compromised (such as when a trusted device is misplaced).146 *147 * If the calling account had been granted `role`, emits a {RoleRevoked}148 * event.149 *150 * Requirements:151 *152 * - the caller must be `account`.153 */154 function renounceRole(bytes32 role, address account) public virtual {155 require(account == _msgSender(), "AccessControl: can only renounce roles for self");156157 _revokeRole(role, account);158 }159160 /**161 * @dev Grants `role` to `account`.162 *163 * If `account` had not been already granted `role`, emits a {RoleGranted}164 * event. Note that unlike {grantRole}, this function doesn't perform any165 * checks on the calling account.166 *167 * Requirements:168 *169 * - this function can only be called from a constructor.170 */171 function _setupRole(bytes32 role, address account) internal virtual {172 require(!address(this).isContract(), "AccessControl: roles cannot be setup after construction");173 _grantRole(role, account);174 }175176 /**177 * @dev Sets `adminRole` as ``role``'s admin role.178 */179 function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {180 _roles[role].adminRole = adminRole;181 }182183 function _grantRole(bytes32