Skip to content

Commit dad7315

Browse files
ernestognwAmxxfrangio
authored
Add AccessControlDefaultAdminRules (#4009)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Francisco <fg@frang.io>
1 parent 2c69f9f commit dad7315

14 files changed

+666
-10
lines changed

.changeset/silent-dancers-type.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`AccessControlDefaultAdminRules`: Add an extension of `AccessControl` with additional security rules for the `DEFAULT_ADMIN_ROLE`.

contracts/access/AccessControl.sol

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ import "../utils/introspection/ERC165.sol";
4444
*
4545
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
4646
* grant and revoke this role. Extra precautions should be taken to secure
47-
* accounts that have been granted it.
47+
* accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
48+
* to enforce additional security measures for this role.
4849
*/
4950
abstract contract AccessControl is Context, IAccessControl, ERC165 {
5051
struct RoleData {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts (last updated v4.8.0) (access/AccessControlDefaultAdminRules.sol)
3+
4+
pragma solidity ^0.8.0;
5+
6+
import "./AccessControl.sol";
7+
import "./IAccessControlDefaultAdminRules.sol";
8+
import "../utils/math/SafeCast.sol";
9+
import "../interfaces/IERC5313.sol";
10+
11+
/**
12+
* @dev Extension of {AccessControl} that allows specifying special rules to manage
13+
* the `DEFAULT_ADMIN_ROLE` holder, which is a sensitive role with special permissions
14+
* over other roles that may potentially have privileged rights in the system.
15+
*
16+
* If a specific role doesn't have an admin role assigned, the holder of the
17+
* `DEFAULT_ADMIN_ROLE` will have the ability to grant it and revoke it.
18+
*
19+
* This contract implements the following risk mitigations on top of {AccessControl}:
20+
*
21+
* * Only one account holds the `DEFAULT_ADMIN_ROLE` since deployment until it's potentially renounced.
22+
* * Enforce a 2-step process to transfer the `DEFAULT_ADMIN_ROLE` to another account.
23+
* * Enforce a configurable delay between the two steps, with the ability to cancel in between.
24+
* - Even after the timer has passed to avoid locking it forever.
25+
* * It is not possible to use another role to manage the `DEFAULT_ADMIN_ROLE`.
26+
*
27+
* Example usage:
28+
*
29+
* ```solidity
30+
* contract MyToken is AccessControlDefaultAdminRules {
31+
* constructor() AccessControlDefaultAdminRules(
32+
* 3 days,
33+
* msg.sender // Explicit initial `DEFAULT_ADMIN_ROLE` holder
34+
* ) {}
35+
*}
36+
* ```
37+
*
38+
* NOTE: The `delay` can only be set in the constructor and is fixed thereafter.
39+
*
40+
* _Available since v4.9._
41+
*/
42+
abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRules, IERC5313, AccessControl {
43+
uint48 private immutable _defaultAdminDelay;
44+
45+
address private _currentDefaultAdmin;
46+
address private _pendingDefaultAdmin;
47+
48+
uint48 private _defaultAdminTransferDelayedUntil;
49+
50+
/**
51+
* @dev Sets the initial values for {defaultAdminDelay} in seconds and {defaultAdmin}.
52+
*
53+
* The `defaultAdminDelay` value is immutable. It can only be set at the constructor.
54+
*/
55+
constructor(uint48 defaultAdminDelay_, address initialDefaultAdmin) {
56+
_defaultAdminDelay = defaultAdminDelay_;
57+
_grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin);
58+
}
59+
60+
/**
61+
* @dev See {IERC5313-owner}.
62+
*/
63+
function owner() public view virtual returns (address) {
64+
return defaultAdmin();
65+
}
66+
67+
/**
68+
* @inheritdoc IAccessControlDefaultAdminRules
69+
*/
70+
function defaultAdminDelay() public view virtual returns (uint48) {
71+
return _defaultAdminDelay;
72+
}
73+
74+
/**
75+
* @inheritdoc IAccessControlDefaultAdminRules
76+
*/
77+
function defaultAdmin() public view virtual returns (address) {
78+
return _currentDefaultAdmin;
79+
}
80+
81+
/**
82+
* @inheritdoc IAccessControlDefaultAdminRules
83+
*/
84+
function pendingDefaultAdmin() public view virtual returns (address) {
85+
return _pendingDefaultAdmin;
86+
}
87+
88+
/**
89+
* @inheritdoc IAccessControlDefaultAdminRules
90+
*/
91+
function defaultAdminTransferDelayedUntil() public view virtual returns (uint48) {
92+
return _defaultAdminTransferDelayedUntil;
93+
}
94+
95+
/**
96+
* @dev See {IERC165-supportsInterface}.
97+
*/
98+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
99+
return interfaceId == type(IAccessControlDefaultAdminRules).interfaceId || super.supportsInterface(interfaceId);
100+
}
101+
102+
/**
103+
* @inheritdoc IAccessControlDefaultAdminRules
104+
*/
105+
function beginDefaultAdminTransfer(address newAdmin) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
106+
_beginDefaultAdminTransfer(newAdmin);
107+
}
108+
109+
/**
110+
* @inheritdoc IAccessControlDefaultAdminRules
111+
*/
112+
function acceptDefaultAdminTransfer() public virtual {
113+
require(_msgSender() == pendingDefaultAdmin(), "AccessControl: pending admin must accept");
114+
_acceptDefaultAdminTransfer();
115+
}
116+
117+
/**
118+
* @inheritdoc IAccessControlDefaultAdminRules
119+
*/
120+
function cancelDefaultAdminTransfer() public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
121+
_resetDefaultAdminTransfer();
122+
}
123+
124+
/**
125+
* @dev Revokes `role` from the calling account.
126+
*
127+
* For `DEFAULT_ADMIN_ROLE`, only allows renouncing in two steps, so it's required
128+
* that the {defaultAdminTransferDelayedUntil} has passed and the pending default admin is the zero address.
129+
* After its execution, it will not be possible to call `onlyRole(DEFAULT_ADMIN_ROLE)`
130+
* functions.
131+
*
132+
* For other roles, see {AccessControl-renounceRole}.
133+
*
134+
* NOTE: Renouncing `DEFAULT_ADMIN_ROLE` will leave the contract without a defaultAdmin,
135+
* thereby disabling any functionality that is only available to the default admin, and the
136+
* possibility of reassigning a non-administrated role.
137+
*/
138+
function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {
139+
if (role == DEFAULT_ADMIN_ROLE) {
140+
require(
141+
pendingDefaultAdmin() == address(0) && _hasDefaultAdminTransferDelayPassed(),
142+
"AccessControl: only can renounce in two delayed steps"
143+
);
144+
}
145+
super.renounceRole(role, account);
146+
}
147+
148+
/**
149+
* @dev See {AccessControl-grantRole}. Reverts for `DEFAULT_ADMIN_ROLE`.
150+
*/
151+
function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {
152+
require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't directly grant default admin role");
153+
super.grantRole(role, account);
154+
}
155+
156+
/**
157+
* @dev See {AccessControl-revokeRole}. Reverts for `DEFAULT_ADMIN_ROLE`.
158+
*/
159+
function revokeRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {
160+
require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't directly revoke default admin role");
161+
super.revokeRole(role, account);
162+
}
163+
164+
/**
165+
* @dev See {AccessControl-_setRoleAdmin}. Reverts for `DEFAULT_ADMIN_ROLE`.
166+
*/
167+
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual override {
168+
require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't violate default admin rules");
169+
super._setRoleAdmin(role, adminRole);
170+
}
171+
172+
/**
173+
* @dev Grants `role` to `account`.
174+
*
175+
* For `DEFAULT_ADMIN_ROLE`, it only allows granting if there isn't already a role's holder
176+
* or if the role has been previously renounced.
177+
*
178+
* For other roles, see {AccessControl-renounceRole}.
179+
*
180+
* NOTE: Exposing this function through another mechanism may make the
181+
* `DEFAULT_ADMIN_ROLE` assignable again. Make sure to guarantee this is
182+
* the expected behavior in your implementation.
183+
*/
184+
function _grantRole(bytes32 role, address account) internal virtual override {
185+
if (role == DEFAULT_ADMIN_ROLE) {
186+
require(defaultAdmin() == address(0), "AccessControl: default admin already granted");
187+
_currentDefaultAdmin = account;
188+
}
189+
super._grantRole(role, account);
190+
}
191+
192+
/**
193+
* @dev See {acceptDefaultAdminTransfer}.
194+
*
195+
* Internal function without access restriction.
196+
*/
197+
function _acceptDefaultAdminTransfer() internal virtual {
198+
require(_hasDefaultAdminTransferDelayPassed(), "AccessControl: transfer delay not passed");
199+
_revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin());
200+
_grantRole(DEFAULT_ADMIN_ROLE, pendingDefaultAdmin());
201+
_resetDefaultAdminTransfer();
202+
}
203+
204+
/**
205+
* @dev See {beginDefaultAdminTransfer}.
206+
*
207+
* Internal function without access restriction.
208+
*/
209+
function _beginDefaultAdminTransfer(address newAdmin) internal virtual {
210+
_defaultAdminTransferDelayedUntil = SafeCast.toUint48(block.timestamp) + defaultAdminDelay();
211+
_pendingDefaultAdmin = newAdmin;
212+
emit DefaultAdminRoleChangeStarted(pendingDefaultAdmin(), defaultAdminTransferDelayedUntil());
213+
}
214+
215+
/**
216+
* @dev See {AccessControl-_revokeRole}.
217+
*/
218+
function _revokeRole(bytes32 role, address account) internal virtual override {
219+
if (role == DEFAULT_ADMIN_ROLE) {
220+
delete _currentDefaultAdmin;
221+
}
222+
super._revokeRole(role, account);
223+
}
224+
225+
/**
226+
* @dev Resets the pending default admin and delayed until.
227+
*/
228+
function _resetDefaultAdminTransfer() private {
229+
delete _pendingDefaultAdmin;
230+
delete _defaultAdminTransferDelayedUntil;
231+
}
232+
233+
/**
234+
* @dev Checks if a {defaultAdminTransferDelayedUntil} has been set and passed.
235+
*/
236+
function _hasDefaultAdminTransferDelayPassed() private view returns (bool) {
237+
uint48 delayedUntil = defaultAdminTransferDelayedUntil();
238+
return delayedUntil > 0 && delayedUntil < block.timestamp;
239+
}
240+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts v4.9.0 (access/IAccessControlDefaultAdminRules.sol)
3+
4+
pragma solidity ^0.8.0;
5+
6+
import "./IAccessControl.sol";
7+
8+
/**
9+
* @dev External interface of AccessControlDefaultAdminRules declared to support ERC165 detection.
10+
*
11+
* _Available since v4.9._
12+
*/
13+
interface IAccessControlDefaultAdminRules is IAccessControl {
14+
/**
15+
* @dev Emitted when a `DEFAULT_ADMIN_ROLE` transfer is started, setting `newDefaultAdmin`
16+
* as the next default admin, which will have rights to claim the `DEFAULT_ADMIN_ROLE`
17+
* after `defaultAdminTransferDelayedUntil` has passed.
18+
*/
19+
event DefaultAdminRoleChangeStarted(address indexed newDefaultAdmin, uint48 defaultAdminTransferDelayedUntil);
20+
21+
/**
22+
* @dev Returns the delay between each `DEFAULT_ADMIN_ROLE` transfer.
23+
*/
24+
function defaultAdminDelay() external view returns (uint48);
25+
26+
/**
27+
* @dev Returns the address of the current `DEFAULT_ADMIN_ROLE` holder.
28+
*/
29+
function defaultAdmin() external view returns (address);
30+
31+
/**
32+
* @dev Returns the address of the pending `DEFAULT_ADMIN_ROLE` holder.
33+
*/
34+
function pendingDefaultAdmin() external view returns (address);
35+
36+
/**
37+
* @dev Returns the timestamp after which the pending default admin can claim the `DEFAULT_ADMIN_ROLE`.
38+
*/
39+
function defaultAdminTransferDelayedUntil() external view returns (uint48);
40+
41+
/**
42+
* @dev Starts a `DEFAULT_ADMIN_ROLE` transfer by setting a pending default admin
43+
* and a timer to pass.
44+
*
45+
* Requirements:
46+
*
47+
* - Only can be called by the current `DEFAULT_ADMIN_ROLE` holder.
48+
*
49+
* Emits a {DefaultAdminRoleChangeStarted}.
50+
*/
51+
function beginDefaultAdminTransfer(address newAdmin) external;
52+
53+
/**
54+
* @dev Completes a `DEFAULT_ADMIN_ROLE` transfer.
55+
*
56+
* Requirements:
57+
*
58+
* - Caller should be the pending default admin.
59+
* - `DEFAULT_ADMIN_ROLE` should be granted to the caller.
60+
* - `DEFAULT_ADMIN_ROLE` should be revoked from the previous holder.
61+
*/
62+
function acceptDefaultAdminTransfer() external;
63+
64+
/**
65+
* @dev Cancels a `DEFAULT_ADMIN_ROLE` transfer.
66+
*
67+
* Requirements:
68+
*
69+
* - Can be called even after the timer has passed.
70+
* - Can only be called by the current `DEFAULT_ADMIN_ROLE` holder.
71+
*/
72+
function cancelDefaultAdminTransfer() external;
73+
}

contracts/access/Ownable.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ abstract contract Ownable is Context {
5353

5454
/**
5555
* @dev Leaves the contract without owner. It will not be possible to call
56-
* `onlyOwner` functions anymore. Can only be called by the current owner.
56+
* `onlyOwner` functions. Can only be called by the current owner.
5757
*
5858
* NOTE: Renouncing ownership will leave the contract without an owner,
59-
* thereby removing any functionality that is only available to the owner.
59+
* thereby disabling any functionality that is only available to the owner.
6060
*/
6161
function renounceOwnership() public virtual onlyOwner {
6262
_transferOwnership(address(0));

contracts/access/README.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ This directory provides ways to restrict who can access the functions of a contr
2323
{{IAccessControlEnumerable}}
2424

2525
{{AccessControlEnumerable}}
26+
27+
{{AccessControlDefaultAdminRules}}

contracts/interfaces/README.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ are useful to interact with third party contracts that implement them.
6464

6565
{{IERC4626}}
6666

67+
{{IERC5313}}
68+
6769
{{IERC5267}}
6870

6971
{{IERC5805}}

docs/modules/ROOT/pages/access-control.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ Every role has an associated admin role, which grants permission to call the `gr
131131

132132
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.
133133

134+
Since it is the admin for all roles by default, and in fact it is also its own admin, this role carries significant risk. To mitigate this risk we provide xref:api:access.adoc#AccessControlDefaultAdminRules[`AccessControlDefaultAdminRules`], a recommended extension of `AccessControl` that adds a number of enforced security measures for this role: the admin is restricted to a single account, with a 2-step transfer procedure with a delay in between steps.
135+
134136
Let's take a look at the ERC20 token example, this time taking advantage of the default admin role:
135137

136138
[source,solidity]

docs/modules/ROOT/pages/extending-contracts.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ contract ModifiedAccessControl is AccessControl {
6666

6767
The `super.revokeRole` statement at the end will invoke ``AccessControl``'s original version of `revokeRole`, the same code that would've run if there were no overrides in place.
6868

69+
NOTE: The same rule is implemented and extended in xref:api:access.adoc#AccessControlDefaultAdminRules[`AccessControlDefaultAdminRules`], an extension that also adds enforced security measures for the `DEFAULT_ADMIN_ROLE`.
70+
6971
[[using-hooks]]
7072
== Using Hooks
7173

0 commit comments

Comments
 (0)