How to Use OpenZeppelin’s New AccessControl Contract
OpenZeppelin

How to Use OpenZeppelin’s New AccessControl Contract

By alexroan | Blockchain Developer | 2 May 2020


Version 3 of OpenZeppelin’s Smart Contracts library is out! In the latest release, they introduce a brand new method of controlling access to functions.


What We Know

Controlling access to certain functions is paramount to ensuring the security of smart contracts, and has been the case in Solidity since the introduction of the Ethereum Virtual Machine.

Developers familiar with OpenZeppelin’s Smart Contract repository know that it already provides options to restrict functionality depending on access level.

The most common is the onlyOwner pattern, administered by the Ownable contract. Another is Openzeppelin’s Roles contract, which enables contracts to define multiple roles before deployment and set rules in each function, ensuring that msg.sender holds the correct role.

Ownable

The onlyOwner pattern is the most commonly used and easily implemented access control method. It’s primitive but highly effective.

It assumes that there is a single administrator of the smart contract, and enables the administrator to transfer ownership to another address.

Extending the Ownable contract allows child contracts to define functions with the onlyOwner custom modifier. These functions require that the sender of the transaction be the single administrator.

function normalFunction() public {
// anyone can call this
}

function restrictedFunction() public onlyOwner {
// only the owner can call this
}

This is a simple example of how to utilize the custom modifier provided by the Ownable contract to restrict function access.

Roles

Despite the popularity and ease of use of the Ownable contract, other OpenZeppelin contracts in the repository only ever used the Roles library for access control. This is because of the flexibility that the Roles library provides over the rigidity of the Ownable contract.

Being a library, it is not extended by child contracts but is used as a tool for adding functionality to data types, with the using statement. The Roles library has three functions that it provides for the Role data type, which it defines itself.

Figure 1 shows the definition of 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];
    }
}

Figure 1: Roles.sol

At the top, you can see the Role struct. This is used by contracts to define multiple roles and their members. The functions add()remove(), and has() are functions that the library uses to interact with a Role struct.

For example, figure 2 shows how a token might use two separate roles, _minters and _burners, to apply access restrictions to certain functions.

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

Figure 2: Implementing Roles

Notice how in the mint() function the require statement ensures that the sender of the message is a minter by using the _minters.has(msg.sender) function.


What’s New

Given that this has been the standard for a while, the big news for developers is that the Roles contract has been removed in the upgrade from version 2.5.x to 3.x.

Principles

The Roles library was somewhat restrictive in the functionality it provided.

Being a library, data storage must be controlled by the importing contract. Ideally, access control should be abstracted away to some degree, where the importing contract needs only worry about the restrictions on each function.

The new AccessControl contract is touted as:

“A one-stop-shop for all authorization needs. It lets you easily define multiple roles with different permissions, as well as which accounts are allowed to grant and revoke each role. It also boosts transparency by enabling enumeration of all privileged accounts in a system.”

The last two points in that statement were not possible with the Roles library.

OpenZeppelin looks to be moving towards a system that is more reminiscent of role-based access control (RBAC) and attribute-based access control (ABAC) standards, prominent in traditional computing security.


Dissecting the Code

Figure 3 shows the AccessControl contract code.

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

Figure 3: AccessControl Definition

The RoleData struct on line 42 uses EnumerableSet (also new to version 3) as the data structure to store members. This allows easy iteration on privileged users.

The struct also stores adminRole as a bytes32 variable. This defines which role acts as administrator over a particular role (i.e. the role that has the ability to act as administrator over this role, granting and revoking this role to users).

Events are now emitted when a role is granted or revoked, defined on lines 57 and 66.

The Roles contract provided just three functions: has()add(), and remove(). Forms of these exist in AccessControl, as well as extra functionality like getting role counts, getting specific members of a role by ID, and the ability to renounce a role.


How to Use It

Figure 2 gave the example of a token contract needing two separate roles, _minters and _burners, using the Roles library. For continuity, we’ll use the same concept and apply the AccessControl contract to do it.

Figure 4 shows the implementation of this.

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

Figure 4: Implementing AccessControl

So, what has changed? For a start, each role is no longer defined in the child contract, as they are stored in the parent. Only the bytes32 IDs exist as constant state variables in the child contract (MINTER_ROLE and BURNER_ROLE in this example).

The _setupRole() is used in the constructor to set the initial administrator of roles, bypassing checks performed by grantRole() in AccessControl (since at the point of construction there is no administrator yet).

Also, instead of calling library functions as extensions on data types (i.e. _minters.has(msg.sender)), the functions are internal in their own right (hasRole(MINTER_ROLE, msg.sender)). This makes the code in the child contract generally more clean and readable.

Abstracting away more of the functionality allows child contracts to build on top of the AccessControl contract easier than was possible with the Roles library.

If you’re interested in further exploring the possibilities, Alberto Cuesta Canada has produced examples of contracts expanding on the basic functionality provided by AccessControl.


Conclusion

The introduction of AccessControl is a big step forward in bringing the Ethereum ecosystem closer to industry standards with regards to system security.

The contract received heavy input from industry experts. I imagine some interesting and complex systems will arise from this contract soon, pushing the mantle even further.

I highly recommend importing OpenZeppelin contracts into your project and expanding on them yourself. You never know what you might learn or stumble upon!

Thanks for reading.


Learn More

If you’re interested in Blockchain Development, I write tutorials, walkthroughs, hints, and tips on how to get started and build a portfolio. Check out this evolving list of Blockchain Development Resources.

If you enjoyed this post and want to learn more about Blockchain Development or the Blockchain Space in general, I highly recommend signing up to the Blockgeeks platform. They have courses on a wide range of topics in the industry, from Coding to Marketing to Trading. It has proven to be an invaluable tool for my development in the Blockchain space.

How do you rate this article?


30

1

alexroan
alexroan

Blockchain Developer


Blockchain Developer
Blockchain Developer

Tutorials, walkthrough, hints and tips on Blockchain Development for all levels of expertise.

Send a $0.01 microtip in crypto to the author, and earn yourself as you read!

20% to author / 80% to me.
We pay the tips from our rewards pool.