@lazy-sol/access-control-upgradeable 1.0.7 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ v1.1.0: Contact Size Optimizations
2
+
3
+ - __Breaking Change:__ Solidity 0.8.4 is now required to compile the contracts (previously was 0.8.2).
4
+ - Replaced "access denied" error with string literal (0.4.22) with the custom error AccessDenied() (0.8.4)
5
+ - Introduced lighter modification of the "AccessControl" RBAC contract – "AccessControlCore" (RBAC-C).
6
+ The 'core' version of the RBAC contract hides three rarely used external functions from the public ABI,
7
+ making them internal and thus reducing the overall compiled implementation size.
8
+
9
+ | old name | old modifier | new name | new modifier |
10
+ |----------------------|--------------|-----------------------|--------------|
11
+ | `isFeatureEnabled()` | `public` | `isFeatureEnabled()` | `internal` |
12
+ | `isSenderInRole()` | `public` | `_isSenderInRole()` | `internal` |
13
+ | `isOperatorInRole()` | `public` | `_isOperatorInRole()` | `internal` |
14
+
15
+ - Added `_requireSenderInRole()` and `_requireAccessCondition()` internal functions for a convenient and gas-efficient
16
+ way of checking the access permissions and throwing the AccessDenied() error.
17
+ - Added the CHANGELOG (this file)
18
+
19
+ v1.0.7:
20
+ - Exported the RBAC behavior
21
+
22
+ v1.0.6:
23
+ - Updated the documentation
24
+
25
+ v1.0.5:
26
+ - Exported RBAC JavaScript constants and functions
27
+
28
+ v1.0.4:
29
+ - Fixed broken dependencies
30
+ - Upgraded npm libraries
31
+ - Added Sepolia network support
32
+
33
+ v1.0.3:
34
+ - Allowed lower versions of Hardhat to be used
35
+
36
+ v1.0.2:
37
+ - Optimized npm dependencies
package/README.md CHANGED
@@ -68,7 +68,7 @@ features and roles. Usually, smart contracts use different values for all the fe
68
68
  next section).
69
69
 
70
70
  Access manager may revoke its own permissions, including the bit 255. Eventually that allows an access manager to let
71
- the smart contract “float freely” and be controlled only by the community (via DAO) or by no one at all.
71
+ the smart contract “float freely” and be controlled only by the community (via the DAO) or by no one at all.
72
72
 
73
73
  ## Comparing with OpenZeppelin
74
74
 
@@ -76,7 +76,7 @@ Both our and OpenZeppelin Access Control implementations feature a similar API t
76
76
  thing".
77
77
 
78
78
  Zeppelin implementation is more flexible:
79
- * it allows setting unlimited number of roles, while current is limited to 256 different roles
79
+ * it allows setting an unlimited number of roles, while current is limited to 256 different roles
80
80
  * it allows setting an admin for each role, while current allows having only one global admin
81
81
 
82
82
  Our implementation is more lightweight:
@@ -120,11 +120,11 @@ npm i -D @lazy-sol/access-control-upgradeable
120
120
  Restricted function is a function with a `public` Solidity modifier access to which is restricted
121
121
  so that only a pre-configured set of accounts can execute it.
122
122
 
123
- 1. Enable role-based access control (RBAC) in a new smart contract
124
- by inheriting the RBAC contract from the [AccessControl](./contracts/UpgradeableAccessControl.sol) contract:
123
+ 1. Enable role-based access control (RBAC) in a new smart contract
124
+ by inheriting the RBAC contract from the [UpgradeableAccessControlCore](./contracts/UpgradeableAccessControlCore.sol) contract:
125
125
  ```solidity
126
126
  import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
127
- import "@lazy-sol/access-control/contracts/UpgradeableAccessControl.sol";
127
+ import "@lazy-sol/access-control/contracts/UpgradeableAccessControlCore.sol";
128
128
 
129
129
  /**
130
130
  * @title Simple ERC20 Implementation (Upgreadable)
@@ -133,29 +133,29 @@ so that only a pre-configured set of accounts can execute it.
133
133
  *
134
134
  * @author Lazy So[u]l
135
135
  */
136
- contract MyUpgradeableERC20Token is ERC20Upgradeable, UpgradeableAccessControl {
136
+ contract MyUpgradeableERC20Token is ERC20Upgradeable, UpgradeableAccessControlCore {
137
137
 
138
138
  ...
139
139
 
140
140
  }
141
141
  ```
142
142
 
143
- 2. Define an access control role with the unique integer value:
143
+ 2. Define an access control role with the unique integer value:
144
144
  ```solidity
145
145
  ...
146
146
 
147
147
  /**
148
148
  * @notice Token creator is responsible for creating (minting)
149
- tokens to an arbitrary address
149
+ * tokens to an arbitrary address
150
150
  * @dev Role ROLE_TOKEN_CREATOR allows minting tokens
151
- (calling `mint` function)
151
+ * (calling `mint` function)
152
152
  */
153
153
  uint32 public constant ROLE_TOKEN_CREATOR = 0x0001_0000;
154
154
 
155
155
  ...
156
156
  ```
157
157
 
158
- 3. Add the `require(isSenderInRole(ROLE_TOKEN_CREATOR), "access denied")"` check into the function body:
158
+ 3. Add the `_requireSenderInRole(ROLE_TOKEN_CREATOR)` check into the function body:
159
159
  ```solidity
160
160
  ...
161
161
 
@@ -164,7 +164,7 @@ so that only a pre-configured set of accounts can execute it.
164
164
  */
165
165
  function _mint(address _to, uint256 _value) internal virtual override {
166
166
  // check if caller has sufficient permissions to mint tokens
167
- require(isSenderInRole(ROLE_TOKEN_CREATOR), "access denied");
167
+ _requireSenderInRole(ROLE_TOKEN_CREATOR);
168
168
 
169
169
  // delegate to super implementation
170
170
  super._mint(_to, _value);
@@ -173,8 +173,8 @@ so that only a pre-configured set of accounts can execute it.
173
173
  ...
174
174
  ```
175
175
 
176
- Note: you could also use the `restrictedTo` modifier in the function declaration instead of the `require`
177
- in the function body if you don't need a custom error message:
176
+ Note: it is also possible to use the `restrictedTo` modifier in the function declaration instead of the `require`
177
+ in the function body if this better suits the coding style:
178
178
  ```solidity
179
179
  ...
180
180
 
@@ -189,6 +189,31 @@ so that only a pre-configured set of accounts can execute it.
189
189
  ...
190
190
  ```
191
191
 
192
+ ### Customizing the Error Message
193
+
194
+ Modifier `restrictedTo()`, internal functions `_requireSenderInRole()` and `_requireAccessCondition()` throw the
195
+ `AccessDenied` denied error when access check fails.
196
+
197
+ It is also possible to use your own custom errors or string messages if needed by leveraging a lower level boolean
198
+ functions `_isSenderInRole()` and `_isOperatorInRole()`:
199
+
200
+ ```solidity
201
+ ...
202
+
203
+ /**
204
+ * @inheritdoc ERC20Upgradeable
205
+ */
206
+ function _mint(address _to, uint256 _value) internal virtual override {
207
+ // check if caller has sufficient permissions to mint tokens
208
+ require(_isSenderInRole(ROLE_TOKEN_CREATOR), "access denied");
209
+
210
+ // delegate to super implementation
211
+ super._mint(_to, _value);
212
+ }
213
+
214
+ ...
215
+ ```
216
+
192
217
  Examples:
193
218
  [AdvancedERC20](https://raw.githubusercontent.com/lazy-sol/advanced-erc20/master/contracts/token/AdvancedERC20.sol),
194
219
  [ERC20v1](https://raw.githubusercontent.com/vgorin/solidity-template/master/contracts/token/upgradeable/ERC20v1.sol),
@@ -1,10 +1,10 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.2;
2
+ pragma solidity >=0.8.4; // custom errors (0.8.4)
3
3
 
4
- import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
4
+ import "./InitializableAccessControlCore.sol";
5
5
 
6
6
  /**
7
- * @title Initializable Role-based Access Control (RBAC)
7
+ * @title Initializable Role-based Access Control (I-RBAC)
8
8
  *
9
9
  * @notice Access control smart contract provides an API to check
10
10
  * if a specific operation is permitted globally and/or
@@ -60,235 +60,7 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
60
60
  *
61
61
  * @author Basil Gorin
62
62
  */
63
- abstract contract InitializableAccessControl is Initializable {
64
- /**
65
- * @dev Privileged addresses with defined roles/permissions
66
- * @dev In the context of ERC20/ERC721 tokens these can be permissions to
67
- * allow minting or burning tokens, transferring on behalf and so on
68
- *
69
- * @dev Maps user address to the permissions bitmask (role), where each bit
70
- * represents a permission
71
- * @dev Bitmask 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
72
- * represents all possible permissions
73
- * @dev 'This' address mapping represents global features of the smart contract
74
- *
75
- * @dev We keep the mapping private to prevent direct writes to it from the inheriting
76
- * contracts, `getRole()` and `updateRole()` functions should be used instead
77
- */
78
- mapping(address => uint256) private userRoles;
79
-
80
- /**
81
- * @dev Empty reserved space in storage. The size of the __gap array is calculated so that
82
- * the amount of storage used by a contract always adds up to the 50.
83
- * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
84
- */
85
- uint256[49] private __gap;
86
-
87
- /**
88
- * @notice Access manager is responsible for assigning the roles to users,
89
- * enabling/disabling global features of the smart contract
90
- * @notice Access manager can add, remove and update user roles,
91
- * remove and update global features
92
- *
93
- * @dev Role ROLE_ACCESS_MANAGER allows modifying user roles and global features
94
- * @dev Role ROLE_ACCESS_MANAGER has single bit at position 255 enabled
95
- */
96
- uint256 public constant ROLE_ACCESS_MANAGER = 0x8000000000000000000000000000000000000000000000000000000000000000;
97
-
98
- /**
99
- * @notice Upgrade manager is responsible for smart contract upgrades,
100
- * see https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable
101
- * see https://docs.openzeppelin.com/contracts/4.x/upgradeable
102
- *
103
- * @dev Role ROLE_UPGRADE_MANAGER allows passing the _authorizeUpgrade() check
104
- * @dev Role ROLE_UPGRADE_MANAGER has single bit at position 254 enabled
105
- */
106
- uint256 public constant ROLE_UPGRADE_MANAGER = 0x4000000000000000000000000000000000000000000000000000000000000000;
107
-
108
- /**
109
- * @dev Bitmask representing all the possible permissions (super admin role)
110
- * @dev Has all the bits are enabled (2^256 - 1 value)
111
- */
112
- uint256 internal constant FULL_PRIVILEGES_MASK = type(uint256).max; // before 0.8.0: uint256(-1) overflows to 0xFFFF...
113
-
114
- /**
115
- * @dev Fired in updateRole() and updateFeatures()
116
- *
117
- * @param operator address which was granted/revoked permissions
118
- * @param requested permissions requested
119
- * @param assigned permissions effectively set
120
- */
121
- event RoleUpdated(address indexed operator, uint256 requested, uint256 assigned);
122
-
123
- /**
124
- * @notice Function modifier making a function defined as public behave as restricted
125
- * (so that only a pre-configured set of accounts can execute it)
126
- *
127
- * @param role the role transaction executor is required to have;
128
- * the function throws an "access denied" exception if this condition is not met
129
- */
130
- modifier restrictedTo(uint256 role) {
131
- // verify the access permission
132
- require(isSenderInRole(role), "access denied");
133
-
134
- // execute the rest of the function
135
- _;
136
- }
137
-
138
- /**
139
- * @dev Creates/deploys the RBAC implementation to be used in a proxy
140
- *
141
- * @dev Note:
142
- * the implementation is already initialized and
143
- * `_postConstruct` is not executable on the implementation
144
- * `_postConstruct` is still available in the context of a proxy
145
- * and should be executed on the proxy deployment (in the same tx)
146
- */
147
- constructor() initializer {}
148
-
149
- /**
150
- * @dev Contract initializer, sets the contract owner to have full privileges
151
- *
152
- * @dev Can be executed only once, reverts when executed second time
153
- *
154
- * @dev IMPORTANT:
155
- * this function SHOULD be executed during proxy deployment (in the same transaction)
156
- *
157
- * @param _owner smart contract owner having full privileges, can be zero
158
- * @param _features initial features mask of the contract, can be zero
159
- */
160
- function _postConstruct(address _owner, uint256 _features) internal virtual onlyInitializing {
161
- // grant owner full privileges
162
- __setRole(_owner, FULL_PRIVILEGES_MASK, FULL_PRIVILEGES_MASK);
163
- // update initial features bitmask
164
- __setRole(address(this), _features, _features);
165
- }
166
-
167
- /**
168
- * @dev Highest version that has been initialized.
169
- * Non-zero value means contract was already initialized.
170
- * @dev see {Initializable}, {reinitializer}.
171
- *
172
- * @return highest version that has been initialized
173
- */
174
- function getInitializedVersion() public view returns(uint64) {
175
- // delegate to `_getInitializedVersion`
176
- return _getInitializedVersion();
177
- }
178
-
179
- /**
180
- * @notice Retrieves globally set of features enabled
181
- *
182
- * @dev Effectively reads userRoles role for the contract itself
183
- *
184
- * @return 256-bit bitmask of the features enabled
185
- */
186
- function features() public view returns (uint256) {
187
- // features are stored in 'this' address mapping of `userRoles`
188
- return getRole(address(this));
189
- }
190
-
191
- /**
192
- * @notice Updates set of the globally enabled features (`features`),
193
- * taking into account sender's permissions
194
- *
195
- * @dev Requires transaction sender to have `ROLE_ACCESS_MANAGER` permission
196
- * @dev Function is left for backward compatibility with older versions
197
- *
198
- * @param _mask bitmask representing a set of features to enable/disable
199
- */
200
- function updateFeatures(uint256 _mask) public {
201
- // delegate call to `updateRole`
202
- updateRole(address(this), _mask);
203
- }
204
-
205
- /**
206
- * @notice Reads the permissions (role) for a given user from the `userRoles` mapping
207
- * (privileged addresses with defined roles/permissions)
208
- * @notice In the context of ERC20/ERC721 tokens these can be permissions to
209
- * allow minting or burning tokens, transferring on behalf and so on
210
- *
211
- * @dev Having a simple getter instead of making the mapping public
212
- * allows enforcing the encapsulation of the mapping and protects from
213
- * writing to it directly in the inheriting smart contracts
214
- *
215
- * @param operator address of a user to read permissions for,
216
- * or self address to read global features of the smart contract
217
- */
218
- function getRole(address operator) public view returns(uint256) {
219
- // read the value from `userRoles` and return
220
- return userRoles[operator];
221
- }
222
-
223
- /**
224
- * @notice Updates set of permissions (role) for a given user,
225
- * taking into account sender's permissions.
226
- *
227
- * @dev Setting role to zero is equivalent to removing an all permissions
228
- * @dev Setting role to `FULL_PRIVILEGES_MASK` is equivalent to
229
- * copying senders' permissions (role) to the user
230
- * @dev Requires transaction sender to have `ROLE_ACCESS_MANAGER` permission
231
- *
232
- * @param operator address of a user to alter permissions for,
233
- * or self address to alter global features of the smart contract
234
- * @param role bitmask representing a set of permissions to
235
- * enable/disable for a user specified
236
- */
237
- function updateRole(address operator, uint256 role) public {
238
- // caller must have a permission to update user roles
239
- require(isSenderInRole(ROLE_ACCESS_MANAGER), "access denied");
240
-
241
- // evaluate the role and reassign it
242
- __setRole(operator, role, _evaluateBy(msg.sender, getRole(operator), role));
243
- }
244
-
245
- /**
246
- * @notice Determines the permission bitmask an operator can set on the
247
- * target permission set
248
- * @notice Used to calculate the permission bitmask to be set when requested
249
- * in `updateRole` and `updateFeatures` functions
250
- *
251
- * @dev Calculated based on:
252
- * 1) operator's own permission set read from userRoles[operator]
253
- * 2) target permission set - what is already set on the target
254
- * 3) desired permission set - what do we want set target to
255
- *
256
- * @dev Corner cases:
257
- * 1) Operator is super admin and its permission set is `FULL_PRIVILEGES_MASK`:
258
- * `desired` bitset is returned regardless of the `target` permission set value
259
- * (what operator sets is what they get)
260
- * 2) Operator with no permissions (zero bitset):
261
- * `target` bitset is returned regardless of the `desired` value
262
- * (operator has no authority and cannot modify anything)
263
- *
264
- * @dev Example:
265
- * Consider an operator with the permissions bitmask 00001111
266
- * is about to modify the target permission set 01010101
267
- * Operator wants to set that permission set to 00110011
268
- * Based on their role, an operator has the permissions
269
- * to update only lowest 4 bits on the target, meaning that
270
- * high 4 bits of the target set in this example is left
271
- * unchanged and low 4 bits get changed as desired: 01010011
272
- *
273
- * @param operator address of the contract operator which is about to set the permissions
274
- * @param target input set of permissions to operator is going to modify
275
- * @param desired desired set of permissions operator would like to set
276
- * @return resulting set of permissions given operator will set
277
- */
278
- function _evaluateBy(address operator, uint256 target, uint256 desired) internal view returns (uint256) {
279
- // read operator's permissions
280
- uint256 p = getRole(operator);
281
-
282
- // taking into account operator's permissions,
283
- // 1) enable the permissions desired on the `target`
284
- target |= p & desired;
285
- // 2) disable the permissions desired on the `target`
286
- target &= FULL_PRIVILEGES_MASK ^ (p & (FULL_PRIVILEGES_MASK ^ desired));
287
-
288
- // return calculated result
289
- return target;
290
- }
291
-
63
+ abstract contract InitializableAccessControl is InitializableAccessControlCore {
292
64
  /**
293
65
  * @notice Checks if requested set of features is enabled globally on the contract
294
66
  *
@@ -296,8 +68,8 @@ abstract contract InitializableAccessControl is Initializable {
296
68
  * @return true if all the features requested are enabled, false otherwise
297
69
  */
298
70
  function isFeatureEnabled(uint256 required) public view returns (bool) {
299
- // delegate call to `__hasRole`, passing `features` property
300
- return __hasRole(features(), required);
71
+ // delegate to internal `_isFeatureEnabled`
72
+ return _isFeatureEnabled(required);
301
73
  }
302
74
 
303
75
  /**
@@ -307,8 +79,8 @@ abstract contract InitializableAccessControl is Initializable {
307
79
  * @return true if all the permissions requested are enabled, false otherwise
308
80
  */
309
81
  function isSenderInRole(uint256 required) public view returns (bool) {
310
- // delegate call to `isOperatorInRole`, passing transaction sender
311
- return isOperatorInRole(msg.sender, required);
82
+ // delegate to internal `_isSenderInRole`
83
+ return _isSenderInRole(required);
312
84
  }
313
85
 
314
86
  /**
@@ -319,42 +91,7 @@ abstract contract InitializableAccessControl is Initializable {
319
91
  * @return true if all the permissions requested are enabled, false otherwise
320
92
  */
321
93
  function isOperatorInRole(address operator, uint256 required) public view returns (bool) {
322
- // delegate call to `__hasRole`, passing operator's permissions (role)
323
- return __hasRole(getRole(operator), required);
324
- }
325
-
326
- /**
327
- * @dev Sets the `assignedRole` role to the operator, logs both `requestedRole` and `actualRole`
328
- *
329
- * @dev Unsafe:
330
- * provides direct write access to `userRoles` mapping without any security checks,
331
- * doesn't verify the executor (msg.sender) permissions,
332
- * must be kept private at all times
333
- *
334
- * @param operator address of a user to alter permissions for,
335
- * or self address to alter global features of the smart contract
336
- * @param requestedRole bitmask representing a set of permissions requested
337
- * to be enabled/disabled for a user specified, used only to be logged into event
338
- * @param assignedRole bitmask representing a set of permissions to
339
- * enable/disable for a user specified, used to update the mapping and to be logged into event
340
- */
341
- function __setRole(address operator, uint256 requestedRole, uint256 assignedRole) private {
342
- // assign the role to the operator
343
- userRoles[operator] = assignedRole;
344
-
345
- // fire an event
346
- emit RoleUpdated(operator, requestedRole, assignedRole);
347
- }
348
-
349
- /**
350
- * @dev Checks if role `actual` contains all the permissions required `required`
351
- *
352
- * @param actual existent role
353
- * @param required required role
354
- * @return true if actual has required role (all permissions), false otherwise
355
- */
356
- function __hasRole(uint256 actual, uint256 required) private pure returns (bool) {
357
- // check the bitmask for the role required and return the result
358
- return actual & required == required;
94
+ // delegate to internal `_isOperatorInRole`
95
+ return _isOperatorInRole(operator, required);
359
96
  }
360
97
  }
@@ -0,0 +1,404 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity >=0.8.4; // custom errors (0.8.4)
3
+
4
+ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
5
+
6
+ /**
7
+ * @title Initializable Role-based Access Control Core (I-RBAC-C)
8
+ *
9
+ * @notice Access control smart contract provides an API to check
10
+ * if a specific operation is permitted globally and/or
11
+ * if a particular user has a permission to execute it.
12
+ *
13
+ * @notice This contract is inherited by other contracts requiring the role-based access control (RBAC)
14
+ * protection for the restricted access functions
15
+ *
16
+ * @notice It deals with two main entities: features and roles.
17
+ *
18
+ * @notice Features are designed to be used to enable/disable public functions
19
+ * of the smart contract (used by a wide audience).
20
+ * @notice User roles are designed to control the access to restricted functions
21
+ * of the smart contract (used by a limited set of maintainers).
22
+ *
23
+ * @notice Terms "role", "permissions" and "set of permissions" have equal meaning
24
+ * in the documentation text and may be used interchangeably.
25
+ * @notice Terms "permission", "single permission" implies only one permission bit set.
26
+ *
27
+ * @notice Access manager is a special role which allows to grant/revoke other roles.
28
+ * Access managers can only grant/revoke permissions which they have themselves.
29
+ * As an example, access manager with no other roles set can only grant/revoke its own
30
+ * access manager permission and nothing else.
31
+ *
32
+ * @notice Access manager permission should be treated carefully, as a super admin permission:
33
+ * Access manager with even no other permission can interfere with another account by
34
+ * granting own access manager permission to it and effectively creating more powerful
35
+ * permission set than its own.
36
+ *
37
+ * @dev Both current and OpenZeppelin AccessControl implementations feature a similar API
38
+ * to check/know "who is allowed to do this thing".
39
+ * @dev Zeppelin implementation is more flexible:
40
+ * - it allows setting unlimited number of roles, while current is limited to 256 different roles
41
+ * - it allows setting an admin for each role, while current allows having only one global admin
42
+ * @dev Current implementation is more lightweight:
43
+ * - it uses only 1 bit per role, while Zeppelin uses 256 bits
44
+ * - it allows setting up to 256 roles at once, in a single transaction, while Zeppelin allows
45
+ * setting only one role in a single transaction
46
+ *
47
+ * @dev This smart contract is designed to be inherited by other
48
+ * smart contracts which require access control management capabilities.
49
+ *
50
+ * @dev Access manager permission has a bit 255 set.
51
+ * This bit must not be used by inheriting contracts for any other permissions/features.
52
+ *
53
+ * @dev This is an initializable version of the RBAC, based on Zeppelin implementation,
54
+ * it can be used for EIP-1167 minimal proxies, for ERC1967 proxies, etc.
55
+ * see https://docs.openzeppelin.com/contracts/4.x/api/proxy#Clones
56
+ * see https://docs.openzeppelin.com/contracts/4.x/upgradeable
57
+ * see https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable
58
+ * see https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786
59
+ * see https://eips.ethereum.org/EIPS/eip-1167
60
+ *
61
+ * @dev The 'core' version of the RBAC contract hides three rarely used external functions from the public ABI,
62
+ * making them internal and thus reducing the overall compiled implementation size.
63
+ * isFeatureEnabled() public -> _isFeatureEnabled() internal
64
+ * isSenderInRole() public -> _isSenderInRole() internal
65
+ * isOperatorInRole() public -> _isOperatorInRole() internal
66
+ *
67
+ * @custom:since 1.1.0
68
+ *
69
+ * @author Basil Gorin
70
+ */
71
+ abstract contract InitializableAccessControlCore is Initializable {
72
+ /**
73
+ * @dev Privileged addresses with defined roles/permissions
74
+ * @dev In the context of ERC20/ERC721 tokens these can be permissions to
75
+ * allow minting or burning tokens, transferring on behalf and so on
76
+ *
77
+ * @dev Maps user address to the permissions bitmask (role), where each bit
78
+ * represents a permission
79
+ * @dev Bitmask 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
80
+ * represents all possible permissions
81
+ * @dev 'This' address mapping represents global features of the smart contract
82
+ *
83
+ * @dev We keep the mapping private to prevent direct writes to it from the inheriting
84
+ * contracts, `getRole()` and `updateRole()` functions should be used instead
85
+ */
86
+ mapping(address => uint256) private userRoles;
87
+
88
+ /**
89
+ * @dev Empty reserved space in storage. The size of the __gap array is calculated so that
90
+ * the amount of storage used by a contract always adds up to the 50.
91
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
92
+ */
93
+ uint256[49] private __gap;
94
+
95
+ /**
96
+ * @notice Access manager is responsible for assigning the roles to users,
97
+ * enabling/disabling global features of the smart contract
98
+ * @notice Access manager can add, remove and update user roles,
99
+ * remove and update global features
100
+ *
101
+ * @dev Role ROLE_ACCESS_MANAGER allows modifying user roles and global features
102
+ * @dev Role ROLE_ACCESS_MANAGER has single bit at position 255 enabled
103
+ */
104
+ uint256 public constant ROLE_ACCESS_MANAGER = 0x8000000000000000000000000000000000000000000000000000000000000000;
105
+
106
+ /**
107
+ * @notice Upgrade manager is responsible for smart contract upgrades,
108
+ * see https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable
109
+ * see https://docs.openzeppelin.com/contracts/4.x/upgradeable
110
+ *
111
+ * @dev Role ROLE_UPGRADE_MANAGER allows passing the _authorizeUpgrade() check
112
+ * @dev Role ROLE_UPGRADE_MANAGER has single bit at position 254 enabled
113
+ */
114
+ uint256 public constant ROLE_UPGRADE_MANAGER = 0x4000000000000000000000000000000000000000000000000000000000000000;
115
+
116
+ /**
117
+ * @dev Bitmask representing all the possible permissions (super admin role)
118
+ * @dev Has all the bits are enabled (2^256 - 1 value)
119
+ */
120
+ uint256 internal constant FULL_PRIVILEGES_MASK = type(uint256).max; // before 0.8.0: uint256(-1) overflows to 0xFFFF...
121
+
122
+ /**
123
+ * @notice Thrown when a function is executed by an account that does not have
124
+ * the required access permission(s) (role)
125
+ *
126
+ * @dev This error is used to enforce role-based access control (RBAC) restrictions
127
+ */
128
+ error AccessDenied();
129
+
130
+ /**
131
+ * @dev Fired in updateRole() and updateFeatures()
132
+ *
133
+ * @param operator address which was granted/revoked permissions
134
+ * @param requested permissions requested
135
+ * @param assigned permissions effectively set
136
+ */
137
+ event RoleUpdated(address indexed operator, uint256 requested, uint256 assigned);
138
+
139
+ /**
140
+ * @notice Function modifier making a function defined as public behave as restricted
141
+ * (so that only a pre-configured set of accounts can execute it)
142
+ *
143
+ * @param role the role transaction executor is required to have;
144
+ * the function throws an "access denied" exception if this condition is not met
145
+ */
146
+ modifier restrictedTo(uint256 role) {
147
+ // verify the access permission
148
+ _requireSenderInRole(role);
149
+
150
+ // execute the rest of the function
151
+ _;
152
+ }
153
+
154
+ /**
155
+ * @dev Creates/deploys the RBAC implementation to be used in a proxy
156
+ *
157
+ * @dev Note:
158
+ * the implementation is already initialized and
159
+ * `_postConstruct` is not executable on the implementation
160
+ * `_postConstruct` is still available in the context of a proxy
161
+ * and should be executed on the proxy deployment (in the same tx)
162
+ */
163
+ constructor() initializer {}
164
+
165
+ /**
166
+ * @dev Contract initializer, sets the contract owner to have full privileges
167
+ *
168
+ * @dev Can be executed only once, reverts when executed second time
169
+ *
170
+ * @dev IMPORTANT:
171
+ * this function SHOULD be executed during proxy deployment (in the same transaction)
172
+ *
173
+ * @param _owner smart contract owner having full privileges, can be zero
174
+ * @param _features initial features mask of the contract, can be zero
175
+ */
176
+ function _postConstruct(address _owner, uint256 _features) internal virtual onlyInitializing {
177
+ // grant owner full privileges
178
+ __setRole(_owner, FULL_PRIVILEGES_MASK, FULL_PRIVILEGES_MASK);
179
+ // update initial features bitmask
180
+ __setRole(address(this), _features, _features);
181
+ }
182
+
183
+ /**
184
+ * @dev Highest version that has been initialized.
185
+ * Non-zero value means contract was already initialized.
186
+ * @dev see {Initializable}, {reinitializer}.
187
+ *
188
+ * @return highest version that has been initialized
189
+ */
190
+ function getInitializedVersion() public view returns(uint64) {
191
+ // delegate to `_getInitializedVersion`
192
+ return _getInitializedVersion();
193
+ }
194
+
195
+ /**
196
+ * @notice Retrieves globally set of features enabled
197
+ *
198
+ * @dev Effectively reads userRoles role for the contract itself
199
+ *
200
+ * @return 256-bit bitmask of the features enabled
201
+ */
202
+ function features() public view returns (uint256) {
203
+ // features are stored in 'this' address mapping of `userRoles`
204
+ return getRole(address(this));
205
+ }
206
+
207
+ /**
208
+ * @notice Updates set of the globally enabled features (`features`),
209
+ * taking into account sender's permissions
210
+ *
211
+ * @dev Requires transaction sender to have `ROLE_ACCESS_MANAGER` permission
212
+ * @dev Function is left for backward compatibility with older versions
213
+ *
214
+ * @param _mask bitmask representing a set of features to enable/disable
215
+ */
216
+ function updateFeatures(uint256 _mask) public {
217
+ // delegate call to `updateRole`
218
+ updateRole(address(this), _mask);
219
+ }
220
+
221
+ /**
222
+ * @notice Reads the permissions (role) for a given user from the `userRoles` mapping
223
+ * (privileged addresses with defined roles/permissions)
224
+ * @notice In the context of ERC20/ERC721 tokens these can be permissions to
225
+ * allow minting or burning tokens, transferring on behalf and so on
226
+ *
227
+ * @dev Having a simple getter instead of making the mapping public
228
+ * allows enforcing the encapsulation of the mapping and protects from
229
+ * writing to it directly in the inheriting smart contracts
230
+ *
231
+ * @param operator address of a user to read permissions for,
232
+ * or self address to read global features of the smart contract
233
+ */
234
+ function getRole(address operator) public view returns(uint256) {
235
+ // read the value from `userRoles` and return
236
+ return userRoles[operator];
237
+ }
238
+
239
+ /**
240
+ * @notice Updates set of permissions (role) for a given user,
241
+ * taking into account sender's permissions.
242
+ *
243
+ * @dev Setting role to zero is equivalent to removing an all permissions
244
+ * @dev Setting role to `FULL_PRIVILEGES_MASK` is equivalent to
245
+ * copying senders' permissions (role) to the user
246
+ * @dev Requires transaction sender to have `ROLE_ACCESS_MANAGER` permission
247
+ *
248
+ * @param operator address of a user to alter permissions for,
249
+ * or self address to alter global features of the smart contract
250
+ * @param role bitmask representing a set of permissions to
251
+ * enable/disable for a user specified
252
+ */
253
+ function updateRole(address operator, uint256 role) public {
254
+ // caller must have a permission to update user roles
255
+ _requireSenderInRole(ROLE_ACCESS_MANAGER);
256
+
257
+ // evaluate the role and reassign it
258
+ __setRole(operator, role, _evaluateBy(msg.sender, getRole(operator), role));
259
+ }
260
+
261
+ /**
262
+ * @notice Determines the permission bitmask an operator can set on the
263
+ * target permission set
264
+ * @notice Used to calculate the permission bitmask to be set when requested
265
+ * in `updateRole` and `updateFeatures` functions
266
+ *
267
+ * @dev Calculated based on:
268
+ * 1) operator's own permission set read from userRoles[operator]
269
+ * 2) target permission set - what is already set on the target
270
+ * 3) desired permission set - what do we want set target to
271
+ *
272
+ * @dev Corner cases:
273
+ * 1) Operator is super admin and its permission set is `FULL_PRIVILEGES_MASK`:
274
+ * `desired` bitset is returned regardless of the `target` permission set value
275
+ * (what operator sets is what they get)
276
+ * 2) Operator with no permissions (zero bitset):
277
+ * `target` bitset is returned regardless of the `desired` value
278
+ * (operator has no authority and cannot modify anything)
279
+ *
280
+ * @dev Example:
281
+ * Consider an operator with the permissions bitmask 00001111
282
+ * is about to modify the target permission set 01010101
283
+ * Operator wants to set that permission set to 00110011
284
+ * Based on their role, an operator has the permissions
285
+ * to update only lowest 4 bits on the target, meaning that
286
+ * high 4 bits of the target set in this example is left
287
+ * unchanged and low 4 bits get changed as desired: 01010011
288
+ *
289
+ * @param operator address of the contract operator which is about to set the permissions
290
+ * @param target input set of permissions to operator is going to modify
291
+ * @param desired desired set of permissions operator would like to set
292
+ * @return resulting set of permissions given operator will set
293
+ */
294
+ function _evaluateBy(address operator, uint256 target, uint256 desired) internal view returns (uint256) {
295
+ // read operator's permissions
296
+ uint256 p = getRole(operator);
297
+
298
+ // taking into account operator's permissions,
299
+ // 1) enable the permissions desired on the `target`
300
+ target |= p & desired;
301
+ // 2) disable the permissions desired on the `target`
302
+ target &= FULL_PRIVILEGES_MASK ^ (p & (FULL_PRIVILEGES_MASK ^ desired));
303
+
304
+ // return calculated result
305
+ return target;
306
+ }
307
+
308
+ /**
309
+ * @notice Ensures that the transaction sender has the required access permission(s) (role)
310
+ *
311
+ * @dev Reverts with an `AccessDenied` error if the sender does not have the required role
312
+ *
313
+ * @param required the set of permissions (role) that the transaction sender is required to have
314
+ */
315
+ function _requireSenderInRole(uint256 required) internal view {
316
+ // check if the transaction has the required permission(s),
317
+ // reverting with the "access denied" error if not
318
+ _requireAccessCondition(_isSenderInRole(required));
319
+ }
320
+
321
+ /**
322
+ * @notice Ensures that a specific condition is met
323
+ *
324
+ * @dev Reverts with an `AccessDenied` error if the condition is not met
325
+ *
326
+ * @param condition the condition that needs to be true for the function to proceed
327
+ */
328
+ function _requireAccessCondition(bool condition) internal pure {
329
+ // check if the condition holds
330
+ if(!condition) {
331
+ // revert with the "access denied" error if not
332
+ revert AccessDenied();
333
+ }
334
+ }
335
+
336
+ /**
337
+ * @notice Checks if requested set of features is enabled globally on the contract
338
+ *
339
+ * @param required set of features to check against
340
+ * @return true if all the features requested are enabled, false otherwise
341
+ */
342
+ function _isFeatureEnabled(uint256 required) internal view returns (bool) {
343
+ // delegate call to `__hasRole`, passing `features` property
344
+ return __hasRole(features(), required);
345
+ }
346
+
347
+ /**
348
+ * @notice Checks if transaction sender `msg.sender` has all the permissions required
349
+ *
350
+ * @param required set of permissions (role) to check against
351
+ * @return true if all the permissions requested are enabled, false otherwise
352
+ */
353
+ function _isSenderInRole(uint256 required) internal view returns (bool) {
354
+ // delegate call to `isOperatorInRole`, passing transaction sender
355
+ return _isOperatorInRole(msg.sender, required);
356
+ }
357
+
358
+ /**
359
+ * @notice Checks if operator has all the permissions (role) required
360
+ *
361
+ * @param operator address of the user to check role for
362
+ * @param required set of permissions (role) to check
363
+ * @return true if all the permissions requested are enabled, false otherwise
364
+ */
365
+ function _isOperatorInRole(address operator, uint256 required) internal view returns (bool) {
366
+ // delegate call to `__hasRole`, passing operator's permissions (role)
367
+ return __hasRole(getRole(operator), required);
368
+ }
369
+
370
+ /**
371
+ * @dev Sets the `assignedRole` role to the operator, logs both `requestedRole` and `actualRole`
372
+ *
373
+ * @dev Unsafe:
374
+ * provides direct write access to `userRoles` mapping without any security checks,
375
+ * doesn't verify the executor (msg.sender) permissions,
376
+ * must be kept private at all times
377
+ *
378
+ * @param operator address of a user to alter permissions for,
379
+ * or self address to alter global features of the smart contract
380
+ * @param requestedRole bitmask representing a set of permissions requested
381
+ * to be enabled/disabled for a user specified, used only to be logged into event
382
+ * @param assignedRole bitmask representing a set of permissions to
383
+ * enable/disable for a user specified, used to update the mapping and to be logged into event
384
+ */
385
+ function __setRole(address operator, uint256 requestedRole, uint256 assignedRole) private {
386
+ // assign the role to the operator
387
+ userRoles[operator] = assignedRole;
388
+
389
+ // fire an event
390
+ emit RoleUpdated(operator, requestedRole, assignedRole);
391
+ }
392
+
393
+ /**
394
+ * @dev Checks if role `actual` contains all the permissions required `required`
395
+ *
396
+ * @param actual existent role
397
+ * @param required required role
398
+ * @return true if actual has required role (all permissions), false otherwise
399
+ */
400
+ function __hasRole(uint256 actual, uint256 required) private pure returns (bool) {
401
+ // check the bitmask for the role required and return the result
402
+ return actual & required == required;
403
+ }
404
+ }
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.2;
2
+ pragma solidity >=0.8.4;
3
3
 
4
4
  import "./InitializableAccessControl.sol";
5
- import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
5
+ import "./UpgradeableAccessControlCore.sol";
6
6
 
7
7
  /**
8
- * @title Upgradeable Role-based Access Control (RBAC) // ERC1967Proxy
8
+ * @title Upgradeable Role-based Access Control (U-RBAC) // ERC1967Proxy
9
9
  *
10
10
  * @notice Access control smart contract provides an API to check
11
11
  * if a specific operation is permitted globally and/or
@@ -58,23 +58,6 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
58
58
  *
59
59
  * @author Basil Gorin
60
60
  */
61
- abstract contract UpgradeableAccessControl is InitializableAccessControl, UUPSUpgradeable {
62
- /**
63
- * @notice Returns an address of the implementation smart contract,
64
- * see ERC1967Upgrade._getImplementation()
65
- *
66
- * @return the current implementation address
67
- */
68
- function getImplementation() public view virtual returns (address) {
69
- // delegate to `ERC1967Upgrade._getImplementation()`
70
- return _getImplementation();
71
- }
72
-
73
- /**
74
- * @inheritdoc UUPSUpgradeable
75
- */
76
- function _authorizeUpgrade(address) internal virtual override {
77
- // caller must have a permission to upgrade the contract
78
- require(isSenderInRole(ROLE_UPGRADE_MANAGER), "access denied");
79
- }
61
+ abstract contract UpgradeableAccessControl is InitializableAccessControl, UpgradeableAccessControlCore {
62
+
80
63
  }
@@ -0,0 +1,88 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity >=0.8.4;
3
+
4
+ import "./InitializableAccessControlCore.sol";
5
+ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
6
+
7
+ /**
8
+ * @title Upgradeable Role-based Access Control Core (U-RBAC-C) // ERC1967Proxy
9
+ *
10
+ * @notice Access control smart contract provides an API to check
11
+ * if a specific operation is permitted globally and/or
12
+ * if a particular user has a permission to execute it.
13
+ *
14
+ * @notice This contract is inherited by other contracts requiring the role-based access control (RBAC)
15
+ * protection for the restricted access functions
16
+ *
17
+ * @notice It deals with two main entities: features and roles.
18
+ *
19
+ * @notice Features are designed to be used to enable/disable public functions
20
+ * of the smart contract (used by a wide audience).
21
+ * @notice User roles are designed to control the access to restricted functions
22
+ * of the smart contract (used by a limited set of maintainers).
23
+ *
24
+ * @notice Terms "role", "permissions" and "set of permissions" have equal meaning
25
+ * in the documentation text and may be used interchangeably.
26
+ * @notice Terms "permission", "single permission" implies only one permission bit set.
27
+ *
28
+ * @notice Access manager is a special role which allows to grant/revoke other roles.
29
+ * Access managers can only grant/revoke permissions which they have themselves.
30
+ * As an example, access manager with no other roles set can only grant/revoke its own
31
+ * access manager permission and nothing else.
32
+ *
33
+ * @notice Access manager permission should be treated carefully, as a super admin permission:
34
+ * Access manager with even no other permission can interfere with another account by
35
+ * granting own access manager permission to it and effectively creating more powerful
36
+ * permission set than its own.
37
+ *
38
+ * @dev Both current and OpenZeppelin AccessControl implementations feature a similar API
39
+ * to check/know "who is allowed to do this thing".
40
+ * @dev Zeppelin implementation is more flexible:
41
+ * - it allows setting unlimited number of roles, while current is limited to 256 different roles
42
+ * - it allows setting an admin for each role, while current allows having only one global admin
43
+ * @dev Current implementation is more lightweight:
44
+ * - it uses only 1 bit per role, while Zeppelin uses 256 bits
45
+ * - it allows setting up to 256 roles at once, in a single transaction, while Zeppelin allows
46
+ * setting only one role in a single transaction
47
+ *
48
+ * @dev This smart contract is designed to be inherited by other
49
+ * smart contracts which require access control management capabilities.
50
+ *
51
+ * @dev Access manager permission has a bit 255 set.
52
+ * This bit must not be used by inheriting contracts for any other permissions/features.
53
+ *
54
+ * @dev This is an upgradeable version of the RBAC, based on Zeppelin implementation for ERC1967,
55
+ * see https://docs.openzeppelin.com/contracts/4.x/upgradeable
56
+ * see https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable
57
+ * see https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786
58
+ *
59
+ * @dev The 'core' version of the RBAC contract hides three rarely used external functions from the public ABI,
60
+ * making them internal and thus reducing the overall compiled implementation size.
61
+ * isFeatureEnabled() public -> _isFeatureEnabled() internal
62
+ * isSenderInRole() public -> _isSenderInRole() internal
63
+ * isOperatorInRole() public -> _isOperatorInRole() internal
64
+ *
65
+ * @custom:since 1.1.0
66
+ *
67
+ * @author Basil Gorin
68
+ */
69
+ abstract contract UpgradeableAccessControlCore is InitializableAccessControlCore, UUPSUpgradeable {
70
+ /**
71
+ * @notice Returns an address of the implementation smart contract,
72
+ * see ERC1967Upgrade._getImplementation()
73
+ *
74
+ * @return the current implementation address
75
+ */
76
+ function getImplementation() public view virtual returns (address) {
77
+ // delegate to `ERC1967Upgrade._getImplementation()`
78
+ return _getImplementation();
79
+ }
80
+
81
+ /**
82
+ * @inheritdoc UUPSUpgradeable
83
+ */
84
+ function _authorizeUpgrade(address) internal virtual override {
85
+ // caller must have a permission to upgrade the contract
86
+ _requireSenderInRole(ROLE_UPGRADE_MANAGER);
87
+ }
88
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.2;
2
+ pragma solidity >=0.8.4;
3
3
 
4
4
  import "../UpgradeableAccessControl.sol";
5
5
 
@@ -10,6 +10,12 @@ contract UpgradeableAccessControlMock is UpgradeableAccessControl {
10
10
  function restricted() public restrictedTo(RESTRICTED_ROLE) {
11
11
  emit Restricted();
12
12
  }
13
+ function requireSenderInRole(uint256 required) public view {
14
+ _requireSenderInRole(required);
15
+ }
16
+ function requireAccessCondition(bool condition) public pure {
17
+ _requireAccessCondition(condition);
18
+ }
13
19
  }
14
20
 
15
21
  contract UpgradeableAccessControl1 is UpgradeableAccessControlMock {
package/hardhat.config.js CHANGED
@@ -50,7 +50,7 @@ module.exports = {
50
50
  // https://hardhat.org/guides/compile-contracts.html
51
51
  compilers: [
52
52
  {
53
- version: "0.8.2",
53
+ version: "0.8.4",
54
54
  settings: {
55
55
  optimizer: {
56
56
  enabled: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazy-sol/access-control-upgradeable",
3
- "version": "1.0.7",
3
+ "version": "1.1.1",
4
4
  "description": "Enable the modular plug and play (PnP) architecture for your dapp by incorporating the role-based access control (RBAC) into the smart contracts",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -22,20 +22,22 @@
22
22
  "author": "Basil Gorin",
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
- "@lazy-sol/a-missing-gem": "^1.0.9",
26
- "@nomiclabs/hardhat-truffle5": "^2.0.7",
27
- "@openzeppelin/contracts-upgradeable": "^4.9.6",
28
- "hardhat": "^2.22.7",
29
- "hardhat-deploy": "^0.11.45"
25
+ "@openzeppelin/contracts-upgradeable": "4.9.6"
30
26
  },
31
27
  "devDependencies": {
28
+ "@lazy-sol/a-missing-gem": "^1.0.10",
32
29
  "@lazy-sol/zeppelin-test-helpers": "^1.0.5",
33
- "@openzeppelin/contracts": "^4.9.6",
30
+ "@nomiclabs/hardhat-truffle5": "^2.0.7",
31
+ "@openzeppelin/contracts": "4.9.6",
32
+ "hardhat": "^2.22.13",
34
33
  "hardhat-dependency-injector": "^1.0.1",
35
34
  "hardhat-gas-reporter": "^1.0.10",
36
- "solidity-coverage": "^0.8.12"
35
+ "solidity-coverage": "^0.8.13"
37
36
  },
38
37
  "overrides": {
38
+ "axios": ">=1.7.5",
39
+ "micromatch": "^4.0.8",
40
+ "tar": "^6.2.1",
39
41
  "tough-cookie": "^4.1.3",
40
42
  "yargs-parser": "^5.0.1",
41
43
  "ws": "^8.0.0"
@@ -41,6 +41,18 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
41
41
  const by = a1;
42
42
  const to = a2;
43
43
 
44
+ describe("requireAccessCondition(condition) pure function", function() {
45
+ let access_control;
46
+ beforeEach(async function() {
47
+ access_control = await deployment_fn.call(this, a0, ZERO_ADDRESS, 0);
48
+ });
49
+ it("throws if condition is false", async function() {
50
+ await expectRevert(access_control.requireAccessCondition(false, {from: a0}), "AccessDenied()");
51
+ });
52
+ it("succeeds if condition is true", async function() {
53
+ await access_control.requireAccessCondition(true, {from: a0});
54
+ });
55
+ });
44
56
  describe("deployment and initial state", function() {
45
57
  function deploy_and_check(owner, features) {
46
58
  let access_control;
@@ -82,7 +94,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
82
94
  deploy_and_check(a1, random_bn256());
83
95
  });
84
96
  });
85
- describe("when deployed with not initial features", function() {
97
+ describe("when deployed with no initial features", function() {
86
98
  let access_control;
87
99
  beforeEach(async function() {
88
100
  access_control = await deployment_fn.call(this, a0);
@@ -93,7 +105,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
93
105
  beforeEach(async function() {
94
106
  await access_control.updateRole(by, ROLE_ACCESS_MANAGER, {from: a0});
95
107
  });
96
- describe("when ACCESS_MANAGER has full set of permissions", function() {
108
+ describe("when ACCESS_MANAGER has the full set of permissions", function() {
97
109
  beforeEach(async function() {
98
110
  await access_control.updateRole(by, MAX_UINT256, {from: a0});
99
111
  });
@@ -272,6 +284,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
272
284
  it("operator becomes an ACCESS_MANAGER", async function() {
273
285
  expect(await access_control.isOperatorInRole(to, ROLE_ACCESS_MANAGER), "operator").to.be.true;
274
286
  expect(await access_control.isSenderInRole(ROLE_ACCESS_MANAGER, {from: to}), "sender").to.be.true;
287
+ await access_control.requireSenderInRole(ROLE_ACCESS_MANAGER, {from: to});
275
288
  });
276
289
  });
277
290
  describe("when ACCESS_MANAGER revokes ACCESS_MANAGER permission from itself", function() {
@@ -281,15 +294,16 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
281
294
  it("operator ceases to be an ACCESS_MANAGER", async function() {
282
295
  expect(await access_control.isOperatorInRole(by, ROLE_ACCESS_MANAGER), "operator").to.be.false;
283
296
  expect(await access_control.isSenderInRole(ROLE_ACCESS_MANAGER, {from: by}), "sender").to.be.false;
297
+ await expectRevert(access_control.requireSenderInRole(ROLE_ACCESS_MANAGER, {from: to}), "AccessDenied()");
284
298
  });
285
299
  });
286
300
  });
287
301
  describe("otherwise (no ACCESS_MANAGER permission)", function() {
288
302
  it("updateFeatures reverts", async function() {
289
- await expectRevert(access_control.updateFeatures(1, {from: by}), "access denied");
303
+ await expectRevert(access_control.updateFeatures(1, {from: by}), "AccessDenied()");
290
304
  });
291
305
  it("updateRole reverts", async function() {
292
- await expectRevert(access_control.updateRole(to, 1, {from: by}), "access denied");
306
+ await expectRevert(access_control.updateRole(to, 1, {from: by}), "AccessDenied()");
293
307
  });
294
308
  });
295
309
  }
@@ -44,7 +44,7 @@ contract('AccessControlUpgradeable (U-RBAC) "restrictedTo" Modifier tests', func
44
44
  access_control = await deploy_access_control(a0);
45
45
  });
46
46
  it("function protected with restrictedTo modifier fails when run not by an admin", async function() {
47
- await expectRevert(access_control.restricted({from: a1}), "access denied");
47
+ await expectRevert(access_control.restricted({from: a1}), "AccessDenied()");
48
48
  });
49
49
  describe("function protected with restrictedTo modifier succeeds when run by admin", async function() {
50
50
  let receipt;
@@ -101,13 +101,10 @@ contract("UpgradeableAccessControl (U-RBAC) Core tests", function(accounts) {
101
101
  const init_data = ac.contract.methods.postConstruct(ZERO_ADDRESS, 0).encodeABI();
102
102
 
103
103
  // and upgrade the implementation
104
- await expectRevert(
105
- ac.upgradeToAndCall(impl2.address, init_data, {from: by}),
106
- "access denied"
107
- );
104
+ await expectRevert(ac.upgradeToAndCall(impl2.address, init_data, {from: by}), "AccessDenied()");
108
105
  });
109
106
  it("implementation upgrade without initialization reverts", async function() {
110
- await expectRevert(ac.upgradeTo(impl2.address, {from: by}), "access denied");
107
+ await expectRevert(ac.upgradeTo(impl2.address, {from: by}), "AccessDenied()");
111
108
  });
112
109
  it("direct initialization of the implementation (bypassing proxy) fails", async function() {
113
110
  await expectRevert(impl1.postConstruct(ZERO_ADDRESS, 0, {from: by}), "Initializable: contract is already initialized");