When writing smart contracts I tend to take an educational approach. Even if they are intended for a production environment I make them as easy to understand as possible. I write contracts to be reusable, but usually they get rewritten for each specific business case.
In this article I’m going to discuss three approaches to permissioning in solidity smart contracts. These approaches are discussed in increasing order of complexity, which is the order in which you should consider them for your project. I include code that you can reuse for each approach.
This article assumes that you are comfortable coding smart contracts in solidity, and using features like inheritance and passing contracts addresses as a parameter. If you are looking for an easier article on smart contract development you can try this one.
The Ownable.sol contract from OpenZeppelin must be one of the most reused contracts out there. In 77 lines it implements:
When coding smart contracts you will inherit from
Ownable
very often. Let’s see how to use Ownable
with an example. Imagine that you want to keep a list of addresses in a contract but you want to be the only one that can add more. Think of it like some kind of registry of people that you trust. You could do something like:contract Whitelist is Ownable {
mapping (address => bool) members;
constructor() public Ownable() {
}
function addMember(address _member)
public
onlyOwner
{
members[_member] = true;
}
}
Inheriting from
Ownable
and calling its constructor on yours ensures that the address deploying your contract is registered as the owner. The onlyOwner
modifier makes a function revert if not called by the address registered as owner.Once you deploy this contract only you or someone that you designate can add new members to the list within.
That's it, in a nutshell. There are a couple more functions but you'll get it if you check the source code. Try to implement something simple with it, it is very easy to understand.
Despite its usefulness, there will be many times when
Ownable
is not enough. Only one address can be the owner at a given time, only the owner gets to decide who can be the new owner, you can only check if you are the owner, not is someone else is.Whitelist.sol keeps a list of addresses which then can be used to restrict functionality or any other purpose. It is very similar in functionality to OpenZeppelin’s Roles.sol, although with some critical differences (check our README).
Whitelist.sol only has three functions:
function isMember(address _member) public view returns(bool);
function addMember(address _member) public onlyOwner;
function removeMember(address _member) public onlyOwner;
With this contract you could, for example, keep a list of approved stakeholders who can be the only recipients for token transfers. You could do something like this:
pragma solidity ^0.5.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../access/Whitelist.sol";
contract ERC20Whitelisted is ERC20 {
Whitelist whitelist;
constructor(address _whitelistAddress) public {
whitelist = Whitelist(_whitelistAddress);
}
function transfer(address account, uint256 amount) public {
require(whitelist.isMember(account), "Account not whitelisted.");
super._transfer(account, amount);
}
}
In the example above, you could also make
ERC20Whitelisted
inherit from both ERC20
and Whitelist
. There are some trade offs that I would be happy to discuss.Simple whitelists can be quite powerful. OpenZeppelin implemented many ERC20 and ERC721 variants using them and managed to provide more functionality than most of us will need. At TechHQ we implemented CementDAO using only whitelists as well.
Sometimes, however, whitelists will also fall short. You might need to have more than one owner for a whitelist. Or you might need to manage many overlapping whitelists. For those cases we have a hierarchical role contract.
We developed RBAC.sol aiming to give multi user functionality like you have in modern shared systems.
At a low level we identify the roles using a
bytes32
argument chosen by the user. Commonly these are identifiable short strings, but you can also use an encrypted value or an address.The roles themselves are a group of member addresses and the identifier of the admin role. Funnily enough we don’t need to store the identifier of the role inside its own struct.
struct Role {
bytes32 adminRoleId;
mapping (address => bool) members;
}
There are now two methods to add a new role and verify if a role exists:
function roleExists(bytes32 _roleId) public view returns(bool);
function addRole(bytes32 _roleId, bytes32 _adminRoleId) public;
And the functions for managing members are the same, only that now the relevant role must be specified:
function isMember(address _member, bytes32 _roleId) public view returns(bool);
function addMember(address _member, bytes32 _roleId) public;
function removeMember(address _member, bytes32 _roleId) public;
addMember
and removeMember
will only succeed if the caller belongs to the administrator role of the role that we are adding members to.addRole
will only succeed if the caller belongs to the role that will administer the role being created.These simple rules will allow to create a hierarchy of roles, which can then be used to implement complex multi user platforms with different permissioning levels or areas.
To dive even deeper into the rabbit hole I suggest starting with this issue from OpenZeppelin. Their codebase and ours is not that different, and you will find there a thorough reasoning for most design decisions even in the cases where we have chosen to go the other way. Their use of
Roles
for contracts like ERC20Mintable
is a great example of an alternative to Whitelist
.Another resource for the brave is the AragonOS ACL contract. Just a glance to the interface shows that they have decided to go farther than anyone else:
function hasPermission(address who, address where, bytes32 what, bytes how) public view returns (bool);
We use the three levels of access control described in this article for the examples in our own @hq20/contracts package, so you should also keep an eye there as well.
When it comes to smart contract implementation it is a good idea to implement only the complexity that is required, and no more. In terms of permissioning there are three distinct levels of complexity:
You can use Ownable.sol for systems that are permissioned for a single user. You can use @openzeppelin/Roles.sol or @hq20/Whitelist.sol for systems that require permissioning users in a group. For systems that require a group hierarchy we have successfully used @hq20/RBAC.sol in the past.
You will have your own requirements and will need to take your own decisions on trade offs. Knowing the design decisions behind each implementation will allow you to either use an existing contract or to modify one for your own use.
Please make sure of letting us know of any feedback. We are developing the @hq20/contracts package to support the coding of real world blockchain applications. We are aiming for our code to be reused and abused, and would be very happy to know how you do that.