Access Control Implementation in Solidity Contracts: Ownable | Roles | AccessControl

OpenZeppelin provides three access control modes for smart contracts: the Ownable contract, the Roles library, and the 3.0 new AccelessControl contract.In this tutorial, we will learn about the differences among the three access control modes and how to use them to enhance the security of Solidity contracts in your own ETAI smart contracts.

Learn to develop with Taifang DApp in the development language you are familiar with: Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart

Controlling access to specific methods of smart contracts is important for the security of smart contracts.Developers familiar with OpenZeppelin's smart contract library know that this library already provides access restriction options based on access level. The most common is the onlyOwner mode of Ownable contract management and the other is OpenZeppelin's Roles library, which allows contracts to define multiple roles before deployment and set rules for each function to ensure that msg.sender has the correct role.A more powerful AccessControl contract was introduced in OpenZeppelin 3.0, which is positioned as a one-stop access control solution.

1. Ownable contract - the simplest and most popular access control mode

The onlyOwner mode is the most common and easily implemented access control method, which is basic but very effective.This pattern assumes that there is a single administrator for the smart contract, which supports the administrator to transfer the new account to another account.

By extending the Ownable contract, subcontracts can use the onlyOwner modifier when defining methods that require the transaction initiation account to be the contract administrator.

The following simple example shows how to limit access to restrictedFunction using the onlyOwner modifier provided by the Ownable contract:

function normalFunction() public {
    // Anyone can call
}

function restrictedFunction() public onlyOwner {
    // Only contract administrators can call
}

2. Roles Library - OpenZeppelin's own favorite access control mode

Although the Ownable contract is simple to use, the other contracts in the OpenZeppelin library use the Roles library for access control.This is because the Roles library provides more flexibility than the Ownable contract.

We use the using statement to introduce the Roles contract library to add functionality to the data type.The Roles library implements three methods for Role data types.Here is the definition of Role:

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];
    }
}

At the beginning of the code, we see the Role structure, which the contract uses to define multiple roles and their members.The methods add(), remove(), have() provide an interface to interact with the Role structure.

For example, the code below shows how to set access control rules for a particular method using two different roles _minters and_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);
    }
}

Note that in the mint() function, the require statement ensures that the initiator of the transaction has the minter role. That is _minters.has(msg.sender).

3. AccessControl Contract - Official One-stop Access Control Solution

Although the Roles library is flexible, it has some limitations.Because it is a Solidity library, its data storage is controlled by contracts introduced, and the ideal implementation should be that contracts introduced into the Roles library only need to be concerned with access control capabilities that each method can implement.

OpenZeppelin 3.0's new AccelessControl contract is officially known as:

A one-stop solution for all authentication needs that allows you to: 1. Easily define a solution that has Multiple roles with different privileges 2, which account to define for role authorization and recycling 3, Enumerate all privileged accounts in the system.

Of these three features, the last two are not supported by the Roles library.It appears that OpenZeppelin is gradually implementing role-based access control and attribute-based access control, which are important standards in traditional computing security.

4. Analysis of AccessControl Contract Code

Here is the code for the AccelessControl contract for OpenZeppelin:

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());
        }
    }
}

The RoleData structure defined in line 42 uses the EnumerableSet (Enumeration Set) added in version 3.0 as the data structure that holds role members, making it possible to enumerate privileged users.

The RoleData structure defines adminRole as a bytes32 variable, which indicates which role can be an administrator of a particular role, that is, responsible for authorization and recycling of members of that role.

In lines 57 and 66 of the code, role authorization and recycling events are triggered, respectively.

The Roles library provides three functions: has(), add(), and remove().AccessControl contracts also include these methods and provide additional functionality, such as getting the number of roles, getting specific members of a specified role, and so on.

5. Implement access control of token contracts by using AccessControl contracts

The previous example of a token contract uses the Roles library to define two different roles _minters and _burners.Here we use AccessControl to do the same.

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);
    }
}

What is the difference between an implementation with the Roles library?

First, you no longer need to define each role in a child contract because these roles are saved in the parent contract.Only ID s of type bytes32 exist as state constants in subcontracts, such as MINTER_ROLE and BURNER_ROLE in this example.

_setupRole() is used to set the initial role administrator in the constructor to skip checks by grantRole() in AccessControl because there was no administrator at the time the contract was created.

Additionally, there is no need for specific data types, such as _minters.has(msg.sender), to be invoked directly in a subcontract, such as hasRole(MINTER_ROLE,msg.sender), to make the subcontract code look cleaner and more readable.

5. Tutorial Summary

In this tutorial, we learned three access control design modes in the OpenZeppelin contract library and how to use them: the Ownable contract, the Roles library, and the 3.0 new Accs sControl contract. Ownable is the simplest and AccessControl is the most powerful. You can choose the appropriate access control to protect your Solidity smart contract according to your needs.

Original Link: Three Access Control Modes for OpenZeppelin-Think Network

Keywords: Blockchain Java PHP Python Attribute

Added by netfrugal on Wed, 06 May 2020 00:12:49 +0300