infrastructure-upgrade/lib/openzeppelin-contracts/docs/modules/ROOT/pages/access-control.adoc
T-Hax 735546619e
init
Signed-off-by: T-Hax <>
2023-04-08 18:46:18 +00:00

188 lines
11 KiB
Plaintext

= 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 https://blog.openzeppelin.com/on-the-parity-wallet-multisig-hack-405a8c12e8f7[steals your whole system].
[[ownership-and-ownable]]
== 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 xref:api:access.adoc#Ownable[`Ownable`] for implementing ownership in your contracts.
[source,solidity]
----
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/access/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 xref:api:access.adoc#Ownable-owner--[`owner`] of an `Ownable` contract is the account that deployed it, which is usually exactly what you want.
Ownable also lets you:
* xref:api:access.adoc#Ownable-transferOwnership-address-[`transferOwnership`] from the owner account to a new one, and
* xref:api:access.adoc#Ownable-renounceOwnership--[`renounceOwnership`] for the owner to relinquish this administrative privilege, a common pattern after an initial stage with centralized administration is over.
WARNING: 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 https://github.com/gnosis/MultiSigWallet[Gnosis Multisig] or https://safe.gnosis.io[Gnosis Safe], an https://aragon.org[Aragon DAO], an https://www.uport.me[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 https://makerdao.com[MakerDAO], use systems similar to this one.
[[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. 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. 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 software 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-access-control]]
=== Using `AccessControl`
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 `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]
----
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
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 ERC20("MyToken", "TKN") {
// Grant the minter role to a specified account
_setupRole(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.
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.
Let's augment our ERC20 token example by also defining a 'burner' role, which lets accounts destroy tokens:
[source,solidity]
----
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
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 ERC20("MyToken", "TKN") {
_setupRole(MINTER_ROLE, minter);
_setupRole(BURNER_ROLE, burner);
}
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);
}
}
----
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 `_setupRole`, an `internal` function that is useful when programmatically assigning 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` 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]
----
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
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);
}
}
----
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.
Dynamic role allocation is 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.
[[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));
}
```