Access Control

Access control—that is, "who is allowed to do this thing"—is incredibly important in the world of smart contracts. The access control of your contract may govern who can mint tokens, vote on proposals, freeze transfers, and many other things. It is therefore critical to understand how you implement it, lest someone else steals your whole system.

Ownership and Ownable

The most common and basic form of access control is the concept of ownership: there’s an account that is the owner of a contract and can do administrative tasks on it. This approach is perfectly reasonable for contracts that have a single administrative user.

OpenZeppelin provides Ownable for implementing ownership in your contracts.

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/ownership/Ownable.sol";

contract MyContract is Ownable {
    function normalThing() public {
        // anyone can call this normalThing()
    }

    function specialThing() public onlyOwner {
        // only the owner can call specialThing()!
    }
}

By default, the owner of an Ownable contract is the account that deployed it, which is usually exactly what you want.

Ownable also lets you:

  • transferOwnership from the owner account to a new one, and

  • renounceOwnership for the owner to relinquish this administrative privilege, a common pattern after an initial stage with centralized administration is over.

Removing the owner altogether will mean that administrative tasks that are protected by onlyOwner will no longer be callable!

Note that a contract can also be the owner of another one! This opens the door to using, for example, a Gnosis Multisig or Gnosis Safe, an Aragon DAO, an ERC725/uPort identity contract, or a totally custom contract that you create.

In this way you can use composability to add additional layers of access control complexity to your contracts. Instead of having a single regular Ethereum account (Externally Owned Account, or EOA) as the owner, you could use a 2-of-3 multisig run by your project leads, for example. Prominent projects in the space, such as MakerDAO, use systems similar to this one.

Role-Based Access Control

While the simplicity of ownership can be useful for simple systems or quick prototyping, different levels of authorization are often needed. An account may be able to ban users from a system, but not create new tokens. Role-Based Access Control (RBAC) offers flexibility in this regard.

In essence, we will be defining multiple roles, each allowed to perform different sets of actions. Instead of onlyOwner everywhere - you will use, for example, onlyAdminRole in some places, and onlyModeratorRole in others. Separately, you will be able to define rules for how accounts can be assignned a role, transfer it, and more.

Most of software development uses access control systems that are role-based: some users are regular users, some may be supervisors or managers, and a few will often have administrative privileges.

Using Roles

OpenZeppelin provides Roles for implementing role-based access control. Its usage is straightforward: for each role that you want to define, you’ll store a variable of type Role, which will hold the list of accounts with that role.

Here’s a simple example of using Roles in an ERC20 token: we’ll define two roles, minters and burners, that will be able to mint new tokens, and burn them, respectively.

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

So clean! By splitting concerns this way, much more granular levels of permission may be implemented than were possible with the simpler ownership approach to access control. Note that an account may have more than one role, if desired.

OpenZeppelin uses Roles extensively with predefined contracts that encode rules for each specific role. A few examples are: ERC20Mintable which uses the MinterRole to determine who can mint tokens, and WhitelistCrowdsale which uses both WhitelistAdminRole and WhitelistedRole to create a set of accounts that can purchase tokens.

This flexibility allows for interesting setups: for example, a MintedCrowdsale expects to be given the MinterRole of an ERC20Mintable in order to work, but the token contract could also extend ERC20Pausable and assign the PauserRole to a DAO that serves as a contingency mechanism in case a vulnerability is discovered in the contract code. Limiting what each component of a system is able to do is known as the principle of least privilege, and is a good security practice.

Usage in OpenZeppelin

You’ll notice that none of the OpenZeppelin contracts use Ownable. Roles is a prefferred solution, because it provides the user of the library with enough flexibility to adapt the provided contracts to their needs.

There are some cases, however, where there’s a direct relationship between contracts. For example, RefundableCrowdsale deploys a RefundEscrow on construction, to hold its funds. For those cases, we’ll use Secondary to create a secondary contract that allows a primary contract to manage it. You could also think of these as auxiliary contracts.