|
@@ -42,64 +42,138 @@ In this way you can use _composability_ to add additional layers of access contr
|
|
[[role-based-access-control]]
|
|
[[role-based-access-control]]
|
|
== Role-Based Access Control
|
|
== 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.
|
|
|
|
|
|
+While the simplicity of _ownership_ can be useful for simple systems or quick prototyping, different levels of authorization are often needed. You may want for an account to have permission to ban users from a system, but not create new tokens. https://en.wikipedia.org/wiki/Role-based_access_control[_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.
|
|
|
|
|
|
+In essence, we will be defining multiple _roles_, each allowed to perform different sets of actions. An account may have, for example, 'moderator', 'minter' or 'admin' roles, which you will then check for instead of simply using `onlyOwner`. Separately, you will be able to define rules for how accounts can be granted a role, have it revoked, 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.
|
|
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]]
|
|
|
|
-=== Using `Roles`
|
|
|
|
|
|
+[[using-access-control]]
|
|
|
|
+=== Using `AccessControl`
|
|
|
|
|
|
-OpenZeppelin provides xref:api:access.adoc#Roles[`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.
|
|
|
|
|
|
+OpenZeppelin Contracts provides xref:api:access.adoc#AccessControl[`AccessControl`] for implementing role-based access control. Its usage is straightforward: for each role that you want to define,
|
|
|
|
+you will create a new _role identifier_ that is used to grant, revoke, and check if an account has that role.
|
|
|
|
|
|
-Here's a simple example of using `Roles` in an xref:tokens.adoc#ERC20[`ERC20` token]: we'll define two roles, `minters` and `burners`, that will be able to mint new tokens, and burn them, respectively.
|
|
|
|
|
|
+Here's a simple example of using `AccessControl` in an xref:tokens.adoc#ERC20[`ERC20` token] to define a 'minter' role, which allows accounts that have it create new tokens:
|
|
|
|
|
|
[source,solidity]
|
|
[source,solidity]
|
|
----
|
|
----
|
|
-pragma solidity ^0.5.0;
|
|
|
|
|
|
+pragma solidity ^0.6.0;
|
|
|
|
|
|
-import "@openzeppelin/contracts/access/Roles.sol";
|
|
|
|
|
|
+import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.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;
|
|
|
|
|
|
+contract MyToken is ERC20, AccessControl {
|
|
|
|
+ // Create a new role identifier for the minter role
|
|
|
|
+ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
|
|
|
+
|
|
|
|
+ constructor(address minter) public {
|
|
|
|
+ // Grant the minter role to a specified account
|
|
|
|
+ _grantRole(MINTER_ROLE, minter);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function mint(address to, uint256 amount) public {
|
|
|
|
+ // Check that the calling account has the minter role
|
|
|
|
+ require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
|
|
|
|
+ _mint(to, amount);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+NOTE: Make sure you fully understand how xref:api:access.adoc#AccessControl[`AccessControl`] works before using it on your system, or copy-pasting the examples from this guide.
|
|
|
|
|
|
- Roles.Role private _minters;
|
|
|
|
- Roles.Role private _burners;
|
|
|
|
|
|
+While clear and explicit, this isn't anything we wouldn't have been able to achieve with `Ownable`. Indeed, where `AccessControl` shines is in scenarios where granular permissions are required, which can be implemented by defining _multiple_ roles.
|
|
|
|
|
|
- constructor(address[] memory minters, address[] memory burners)
|
|
|
|
- ERC20Detailed("MyToken", "MTKN", 18)
|
|
|
|
- public
|
|
|
|
- {
|
|
|
|
- for (uint256 i = 0; i < minters.length; ++i) {
|
|
|
|
- _minters.add(minters[i]);
|
|
|
|
- }
|
|
|
|
|
|
+Let's augment our ERC20 token example by also defining a 'burner' role, which lets accounts destroy tokens:
|
|
|
|
|
|
- for (uint256 i = 0; i < burners.length; ++i) {
|
|
|
|
- _burners.add(burners[i]);
|
|
|
|
- }
|
|
|
|
|
|
+[source,solidity]
|
|
|
|
+----
|
|
|
|
+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(address minter, address burner) public {
|
|
|
|
+ _grantRole(MINTER_ROLE, minter);
|
|
|
|
+ _grantRole(BURNER_ROLE, burner);
|
|
}
|
|
}
|
|
|
|
|
|
function mint(address to, uint256 amount) public {
|
|
function mint(address to, uint256 amount) public {
|
|
- // Only minters can mint
|
|
|
|
- require(_minters.has(msg.sender), "DOES_NOT_HAVE_MINTER_ROLE");
|
|
|
|
-
|
|
|
|
|
|
+ require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
|
|
_mint(to, amount);
|
|
_mint(to, amount);
|
|
}
|
|
}
|
|
|
|
|
|
function burn(address from, uint256 amount) public {
|
|
function burn(address from, uint256 amount) public {
|
|
- // Only burners can burn
|
|
|
|
- require(_burners.has(msg.sender), "DOES_NOT_HAVE_BURNER_ROLE");
|
|
|
|
|
|
+ require(hasRole(BURNER_ROLE, msg.sender), "Caller is not a burner");
|
|
|
|
+ _burn(from, amount);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+So clean! By splitting concerns this way, more granular levels of permission may be implemented than were possible with the simpler _ownership_ approach to access control. Limiting what each component of a system is able to do is known as the https://en.wikipedia.org/wiki/Principle_of_least_privilege[principle of least privilege], and is a good security practice. Note that each account may still have more than one role, if so desired.
|
|
|
|
+
|
|
|
|
+[[granting-and-revoking]]
|
|
|
|
+=== Granting and Revoking Roles
|
|
|
|
|
|
|
|
+The ERC20 token example above uses `\_grantRole`, an `internal` function that is useful when programmatically asigning roles (such as during construction). But what if we later want to grant the 'minter' role to additional accounts?
|
|
|
|
+
|
|
|
|
+By default, **accounts with a role cannot grant it or revoke it from other accounts**: all having a role does is making the `hasRole` check pass. To grant and revoke roles dynamically, you will need help from the _role's admin_.
|
|
|
|
+
|
|
|
|
+Every role has an associated admin role, which grants permission to call the `grantRole` and `revokeRole` `external` functions. A role can be granted or revoked by using these if the calling account has the corresponding admin role. Multiple roles may have the same admin role to make management easier. A role's admin can even be the same role itself, which would cause accounts with that role to be able to also grant and revoke it.
|
|
|
|
+
|
|
|
|
+This mechanism can be used to create complex permissioning structures resembling organizational charts, but it also provides an easy way to manage simpler applications. `AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, which acts as the **default admin role for all roles**. An account with this role will be able to manage any other role, unless `\_setRoleAdmin` is used to select a new admin role.
|
|
|
|
+
|
|
|
|
+Let's take a look at the ERC20 token example, this time taking advantage of the default admin role:
|
|
|
|
+
|
|
|
|
+[source,solidity]
|
|
|
|
+----
|
|
|
|
+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 {
|
|
|
|
+ // Grant the contract deployer the default admin role: it will be able
|
|
|
|
+ // to grant and revoke any roles
|
|
|
|
+ _grantRole(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);
|
|
_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.
|
|
|
|
|
|
+Note that, unlike the previous examples, no accounts are granted the 'minter' or 'burner' roles. However, because those roles' admin role is the default admin role, and _that_ role was granted to `msg.sender`, that same account can call `grantRole` to give minting or burning permission, and `revokeRole` to remove it.
|
|
|
|
|
|
-OpenZeppelin uses `Roles` extensively with predefined contracts that encode rules for each specific role. A few examples are: xref:api:token/ERC20.adoc#ERC20Mintable[`ERC20Mintable`] which uses the xref:api:access.adoc#MinterRole[`MinterRole`] to determine who can mint tokens, and xref:api:crowdsale.adoc#WhitelistCrowdsale[`WhitelistCrowdsale`] which uses both xref:api:access.adoc#WhitelistAdminRole[`WhitelistAdminRole`] and xref:api:access.adoc#WhitelistedRole[`WhitelistedRole`] to create a set of accounts that can purchase tokens.
|
|
|
|
|
|
+Dynamic role allocation is a often a desirable property, for example in systems where trust in a participant may vary over time. It can also be used to support use cases such as https://en.wikipedia.org/wiki/Know_your_customer[KYC], where the list of role-bearers may not be known up-front, or may be prohibitively expensive to include in a single transaction.
|
|
|
|
|
|
-This flexibility allows for interesting setups: for example, a xref:api:crowdsale.adoc#MintedCrowdsale[`MintedCrowdsale`] expects to be given the `MinterRole` of an `ERC20Mintable` in order to work, but the token contract could also extend xref:api:token/ERC20.adoc#ERC20Pausable[`ERC20Pausable`] and assign the xref:api:access.adoc#PauserRole[`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 https://en.wikipedia.org/wiki/Principle_of_least_privilege[principle of least privilege], and is a good security practice.
|
|
|
|
|
|
+[[querying-privileged-accounts]]
|
|
|
|
+=== Querying Privileged Accounts
|
|
|
|
+
|
|
|
|
+Because accounts might <<granting-and-revoking, grant and revoke roles>> dynamically, it is not always possible to determine which accounts hold a particular role. This is important as it allows to prove certain properties about a system, such as that an administrative account is a multisig or a DAO, or that a certain role has been removed from all users, effectively disabling any associated functionality.
|
|
|
|
+
|
|
|
|
+Under the hood, `AccessControl` uses `EnumerableSet`, a more powerful variant of Solidity's `mapping` type, which allows for key enumeration. `getRoleMemberCount` can be used to retrieve the number of accounts that have a particular role, and `getRoleMember` can then be called to get the address of each of these accounts.
|
|
|
|
+
|
|
|
|
+```javascript
|
|
|
|
+const minterCount = await myToken.getRoleMemberCount(MINTER_ROLE);
|
|
|
|
+
|
|
|
|
+const members = [];
|
|
|
|
+for (let i = 0; i < minterCount; ++i) {
|
|
|
|
+ members.push(await myToken.getRoleMember(MINTER_ROLE, i));
|
|
|
|
+}
|
|
|
|
+```
|