Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AccessManager guide #4691

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
419fc00
Add AccessManager guide
ernestognw Oct 18, 2023
4849dbb
Codespell
ernestognw Oct 18, 2023
aab6c32
Fix contract identifiers
ernestognw Oct 18, 2023
03437c7
Update contracts/mocks/docs/access-control/AccessManagedERC20MintBase…
ernestognw Oct 20, 2023
1d72816
Corrections
ernestognw Oct 20, 2023
f1c6b37
Update
ernestognw Oct 20, 2023
f737706
Update docs/modules/ROOT/pages/access-control.adoc
ernestognw Oct 26, 2023
326e4f7
Update contracts/mocks/docs/access-control/AccessControlERC20MintMiss…
ernestognw Oct 26, 2023
d0290e2
Update docs/modules/ROOT/pages/access-control.adoc
ernestognw Oct 26, 2023
2947618
Update docs/modules/ROOT/pages/access-control.adoc
ernestognw Oct 26, 2023
584888f
Update docs/modules/ROOT/pages/access-control.adoc
ernestognw Oct 26, 2023
a709051
Update docs/modules/ROOT/pages/access-control.adoc
ernestognw Oct 26, 2023
d54983c
Update docs/modules/ROOT/pages/access-control.adoc
ernestognw Oct 26, 2023
8c79f0b
Update docs/modules/ROOT/pages/access-control.adoc
ernestognw Oct 26, 2023
44f98c4
Update docs/modules/ROOT/pages/access-control.adoc
ernestognw Oct 26, 2023
600cb0d
Add `view` modifier to `proxyAdmin` in TransparentUpgradeableProxy (#…
ernestognw Oct 17, 2023
da02c39
Migrate Ownable tests (#4657)
Amxx Oct 17, 2023
1672d3a
Migrate `MerkleProof` tests among other testing utilities (#4689)
Amxx Oct 23, 2023
bb2bcf8
Migrate `AccessControl` tests (#4694)
ernestognw Oct 26, 2023
5e0018f
Merge branch 'master' into docs/add-access-manager-guide
ernestognw Oct 26, 2023
83dafac
Update docs/modules/ROOT/pages/access-control.adoc
ernestognw Oct 27, 2023
adc4426
General improvements
ernestognw Oct 27, 2023
1d5043d
Apply suggestions from code review
ernestognw Nov 2, 2023
d01776c
Apply suggestions from code review
ernestognw Nov 6, 2023
4b29fce
Merge branch 'master' into docs/add-access-manager-guide
ernestognw Nov 6, 2023
b740f76
Apply more suggestions
ernestognw Nov 6, 2023
75057a3
Apply more suggestions
ernestognw Nov 6, 2023
f86742e
Access Manager -> AccessManager
ernestognw Nov 6, 2023
abaab0d
Access Manager -> AccessManager
ernestognw Nov 6, 2023
b95cda6
Fix links and apply more suggestions
ernestognw Nov 6, 2023
647a5e8
Apply suggestions from code review
ernestognw Nov 6, 2023
4b96759
Merge
ernestognw Nov 7, 2023
be6e909
Format js code as Ethers
ernestognw Nov 8, 2023
de39ada
Apply suggestions from code review
ernestognw Nov 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions contracts/mocks/docs/access-control/AccessControlERC20MintBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessControl} from "../../../access/AccessControl.sol";
import {ERC20} from "../../../token/ERC20/ERC20.sol";

contract AccessControlERC0Mint is ERC20, AccessControl {
// Create a new role identifier for the minter role
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

error CallerNotMinter(address caller);

constructor(address minter) ERC20("MyToken", "TKN") {
// 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
if (!hasRole(MINTER_ROLE, msg.sender)) {
revert CallerNotMinter(msg.sender);
}
_mint(to, amount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessControl} from "../../../access/AccessControl.sol";
import {ERC20} from "../../../token/ERC20/ERC20.sol";

contract AccessControlERC0Mint is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

constructor() ERC20("MyToken", "TKN") {
// 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 onlyRole(MINTER_ROLE) {
_mint(to, amount);
}

function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
_burn(from, amount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessControl} from "../../../access/AccessControl.sol";
import {ERC20} from "../../../token/ERC20/ERC20.sol";

contract AccessControlERC20Mint is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

constructor(address minter, address burner) ERC20("MyToken", "TKN") {
_grantRole(MINTER_ROLE, minter);
_grantRole(BURNER_ROLE, burner);
}

function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}

function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
_burn(from, amount);
}
}
16 changes: 16 additions & 0 deletions contracts/mocks/docs/access-control/AccessManagedERC20MintBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessManaged} from "../../../access/manager/AccessManaged.sol";
import {ERC20} from "../../../token/ERC20/ERC20.sol";

contract AccessManagedERC0Mint is ERC20, AccessManaged {
constructor(address manager) ERC20("MyToken", "TKN") AccessManaged(manager) {}

// Minting is restricted according to the manager rules for this function.
// The function is identified by its selector: 0x40c10f19.
// Calculated with bytes4(keccak256('mint(address,uint256)'))
function mint(address to, uint256 amount) public restricted {
_mint(to, amount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

pragma solidity ^0.8.20;

import {Ownable} from "../../access/Ownable.sol";
import {Ownable} from "../../../access/Ownable.sol";

contract MyContract is Ownable {
constructor(address initialOwner) Ownable(initialOwner) {}
Expand Down
97 changes: 97 additions & 0 deletions docs/modules/ROOT/images/access-control-multiple.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions docs/modules/ROOT/images/access-manager-functions.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 99 additions & 0 deletions docs/modules/ROOT/images/access-manager.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
203 changes: 129 additions & 74 deletions docs/modules/ROOT/pages/access-control.adoc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider renaming this whole page to Access Management instead of Access Control, and then talking about AccessManager first as the recommended approach for any projects that will become complex. WDYT?

I like how "Better role management can be achieved with an xref:api:access.adoc#AccessManager[AccessManager] instance. Instead of managing each contract's permission separately, AccessManager stores all the permissions in a single contract, making your protocol easier to audit and maintain."

Copy link
Member Author

@ernestognw ernestognw Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I proposed this to Fran but he disagreed. I think the section is fine defined as "Access Control" because it refers to the broader AccessControl category in security, and I'd be worried for the SEO if we change it.

Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ The most common and basic form of access control is the concept of _ownership_:
OpenZeppelin Contracts provides xref:api:access.adoc#Ownable[`Ownable`] for implementing ownership in your contracts.

```solidity
include::api:example$MyContractOwnable.sol[]
include::api:example$access-control/MyContractOwnable.sol[]
```

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.
At deployment, the xref:api:access.adoc#Ownable-owner--[`owner`] of an `Ownable` contract is set to the provided `initialOwner` parameter.

Ownable also lets you:

Expand All @@ -22,7 +22,7 @@ Ownable also lets you:

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://gnosis-safe.io[Gnosis Safe], an https://aragon.org[Aragon DAO], or a totally custom contract that _you_ create.
Note that *a contract can also be the owner of another one*! This opens the door to using, for example, a https://safe.global/wallet[Safe Wallet], an https://aragon.org[Aragon DAO], 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.

Expand All @@ -45,28 +45,7 @@ Here's a simple example of using `AccessControl` in an xref:tokens.adoc#ERC20[`E

[source,solidity]
----
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ERC20} from "@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) ERC20("MyToken", "TKN") {
// 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);
}
}
include::api:example$access-control/AccessControlERC20MintBase.sol[]
----

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.
Expand All @@ -77,30 +56,7 @@ Let's augment our ERC20 token example by also defining a 'burner' role, which le

[source,solidity]
----
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ERC20} from "@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) ERC20("MyToken", "TKN") {
_grantRole(MINTER_ROLE, minter);
_grantRole(BURNER_ROLE, burner);
}

function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}

function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
_burn(from, amount);
}
}
include::api:example$access-control/AccessControlERC20MintOnlyRole.sol[]
----

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.
Expand All @@ -122,31 +78,7 @@ Let's take a look at the ERC20 token example, this time taking advantage of the

[source,solidity]
----
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ERC20} from "@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() ERC20("MyToken", "TKN") {
// 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 onlyRole(MINTER_ROLE) {
_mint(to, amount);
}

function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
_burn(from, amount);
}
}
include::api:example$access-control/AccessControlERC20MintMissing.sol[]
----

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.
Expand Down Expand Up @@ -202,3 +134,126 @@ TIP: A recommended configuration is to grant both roles to a secure governance c
Operations executed by the xref:api:governance.adoc#TimelockController[`TimelockController`] are not subject to a fixed delay but rather a minimum delay. Some major updates might call for a longer delay. For example, if a delay of just a few days might be sufficient for users to audit a minting operation, it makes sense to use a delay of a few weeks, or even a few months, when scheduling a smart contract upgrade.

The minimum delay (accessible through the xref:api:governance.adoc#TimelockController-getMinDelay--[`getMinDelay`] method) can be updated by calling the xref:api:governance.adoc#TimelockController-updateDelay-uint256-[`updateDelay`] function. Bear in mind that access to this function is only accessible by the timelock itself, meaning this maintenance operation has to go through the timelock itself.

[[access-management]]
== Access Management

Although xref:api:access.adoc#AccessControl[`AccessControl`] offers a more dynamic solution for permissioning your contracts, decentralized protocoles tend become more complex after integrating new smart contract instances, requiring to keep track of each system smart contract permissions. This increases complexity of uniformly monitoring and updating permissions across the system.

image::access-control-multiple.svg[Access Control multiple]

These protocols often require a single main interface to manage all roles, while keeping the benefits of delaying access control operations and the same built-in rules for assigning and revoking roles.

This is achieved through an xref:api:access.adoc#AccessManager[`AccessManager`] instance, a contract to store the permissions of a system. Instead of managing each contract separately, it allows you to manage permissions from a single contract, making your system easier to audit and control.

image::access-manager.svg[Access Manager]

The manager allows you to permission roles scoped by contract (called targets) and https://docs.soliditylang.org/en/v0.8.20/abi-spec.html#function-selector[function selector] (called functions). In this model, each role is allowed to call multiple functions distributed across contracts. Similarly, a role can be granted to multiple addresses, and a single address can be granted with multiple roles.

image::access-manager-functions.svg[Access Manager functions]

=== Using `AccessManager`

Contracts includes xref:api:access.adoc#AccessManager[`AccessManager`], it can be deployed and used out of the box. It allows setting an initial admin in the constructor, who will be allowed to perform management operations.

For a fresh new contract, you must implement the xref:api:access.adoc#AccessManaged[`AccessManaged`] contract provided along with the manager for matching permissions. This contract should define an Access Manager as its xref:api:access.adoc#AccessManaged-authority--[authority].

Here's a simple example of an xref:tokens.adoc#ERC20[`ERC20`] token that defines a `mint` functionality that is restricted by an xref:api:access.adoc#AccessManager[`AccessManager`]:

```solidity
include::api:example$access-control/AccessManagedERC20MintBase.sol[]
```

NOTE: Make sure you fully understand how xref:api:access.adoc#AccessManager[`AccessManager`] works before using it or copy-pasting the examples from this guide.

Once the managed contract has been deployed, it is now under the manager control, which will get the minter role assigned to an address and also allow such role to call the `mint` function.

```javascript
const MINTER = 42n; // Roles are uint64 (0 is reserved for the ADMIN_ROLE)

// Allow the minter role to call the function selector
// corresponding to the mint function
await manager.setTargetFunctionRole(
myToken.address,
['0x40c10f19'],
MINTER,
{ from: initialAdmin }
);
```

Even though each role has its own list of permissioned functions. Each address granted with a role (called member) will have an execution delay that will dictate how long the account should wait to execute a function allowed to its role. Delayed operations are possible by using the xref:api:access.adoc#AccessManager-schedule-address-bytes-uint48-[`schedule`] and xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] functions.

Additionally, each role granted to a member will have a granting delay before it takes effect. This is how you can set roles with a granting and execution delay:

```javascript
const HOUR = 60 * 60;

const GRANT_DELAY = 24 * HOUR;
const EXECUTION_DELAY = 5 * HOUR;

await manager.setGrantDelay(MINTER, GRANT_DELAY, { from: initialAdmin });
await manager.grantRole(MINTER, EXECUTION_DELAY, { from: initialAdmin });
```

You may notice roles do not define a name. Opposed to the xref:api:access.adoc#AccessControl[`AccessControl`] case, roles aren't hardcoded in the contract as `bytes32` values but identified as numeric values. However, role labeling allows tooling discoverability for easier role exploration and it can be achieved using the xref:api:access.adoc#AccessManager-labelRole-uint64-string-[`labelRole`] function.

```javascript
await manager.labelRole(MINTER, "MINTER");
```

And finally, the manager comes with a configurable `close` mode. When enabled by admins, it restricts every call to any managed contract while keeping permissions intact.

It's recommended to keep only a single admin address secured under a multisig or governance layer. To achieve this, it is possible for the initial admin to set up all the required permissions, targets and functions, assign a new admin and finally renouncing to the admin role.

WARNING: Consider that even if a manager defines permissions for a function. They won't be applied if the managed contract instance is not using the xref:api:access.adoc#AccessManageddf-restricted--[`restricted`] modifier.

=== Role Admins and Guardians

An important aspect of the AccessControl contract is that roles aren't granted nor revoked by role members. Instead, it relies on the concept of a role admin for granting and revoking.

In the case of the AccessMananger, the same rule applies and only each role admin is able to call xref:api:access.adoc#AccessManager-grantRole-uint64-address-uint32-[grant] and http://localhost:8080/contracts/5.x/api/access.html#AccessManager-revokeRole-uint64-address-[revoke] functions. Also note that calling these functions will be subject to the execution delay the executing role admin has.

Additionally, the access manager stores a _guardian_ an extra protection for each role. The guardian is given the ability of canceling operations that have been scheduled by any role member with an execution delay.

Also, consider that a role will have an initial admin and guardian. Both default to the `ADMIN_ROLE` (`0`).

NOTE: Be careful with who's member of the `ADMIN_ROLE` since it acts as the default admin and guardian for every role. Even a misbehaved guardian can cancel operations at discretion, affecting the manager operation.

=== Manager configuration

The manager contract provides a built-in interface for configuring permission settings. These functions can be accessed for those addresses member of the `ADMIN_ROLE`, which is constantly defined as `0` for all managers.

These functions include:

* Add a label to a role using the xref:api:access.doc#AccessManager-labelRole-uint64-string-[`labelRole`] function.
* Assign the admin and guardian of a role with xref:api:access.doc#AccessManager-setRoleAdmin-uint64-uint64-[`setRoleAdmin`] and xref:api:access.doc#AccessManager-setRoleGuardian-uint64-uint64-[`setRoleGuardian`].
* Set each role's grant delay via xref:api:access.doc#AccessManager-setGrantDelay-uint64-uint32-[`setGrantDelay`].

As an admin, some actions you can make will require a delay. Similar to each member's execution delay, some admin operations require waiting for execution and should follow the xref:api:access.adoc#AccessManager-schedule-address-bytes-uint48-[`schedule`] and xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] workflow.

These delayed functions are settings scoped to a target, so each target will have a different admin delay that can be adjusted by admins with xref:api:access.doc#AccessManager-setTargetAdminDelay-address-uint32-[`setTargetAdminDelay`]. These actions are:

* Update a managed contract xref:api:access.adoc#AccessManaged-authority--[authority] using xref:api:access.adoc#AccessManager-updateAuthority-address-address-[`updateAuthority`].
* Close and open a target via xref:api:access.adoc#AccessManager-setTargetClosed-address-bool-[`setTargetClosed`].
* Allow a role to call a target function with xref:api:access.adoc#AccessManager-setTargetFunctionRole-address-bytes4---uint64-[`setTargetFunctionRole`].

=== Using with Ownable

Contracts already inheriting from xref:api:access.adoc#Ownable[`Ownable`] can migrate to the Access Manager by transferring ownership to the manager. After that, the ownable contract will be called through the xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] function. Even if the caller doesn't require a delay.

```javascript
ownable.transferOwnership(accessManager.address, { from: owner });
```

=== Using with AccessControl

For those systems already using xref:api:access.adoc#AccessControl[`AccessControl`], the `DEFAULT_ADMIN_ROLE` can be granted to the AccessManager after revoking every other role. Subsequent calls should be made through the manager's xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] method, similar to the ownable case.

```javascript
// Revoke old roles
accessControl.revokeRoke(MINTER_ROLE, account, { from: admin });

// Grant the admin role to the access manager
accessControl.grantRole(DEFAULT_ADMIN_ROLE, accessManager.address, { from: admin });
accessControl.renounceRole(DEFAULT_ADMIN_ROLE, admin, { from: admin });
```