@lazy-sol/access-control-upgradeable 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/contracts/InitializableAccessControl.sol +10 -273
- package/contracts/InitializableAccessControlCore.sol +404 -0
- package/contracts/UpgradeableAccessControl.sol +5 -22
- package/contracts/UpgradeableAccessControlCore.sol +88 -0
- package/contracts/mocks/UpgradeableAccessControlMocks.sol +7 -1
- package/hardhat.config.js +1 -1
- package/index.js +4 -0
- package/package.json +10 -7
- package/test/include/rbac.behaviour.js +20 -5
- package/test/rbac_modifier.js +1 -1
- package/test/rbac_upgradeable.js +2 -5
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
|
@@ -1,10 +1,10 @@
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
2
|
-
pragma solidity
|
2
|
+
pragma solidity >=0.8.4; // custom errors (0.8.4)
|
3
3
|
|
4
|
-
import "
|
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
|
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
|
300
|
-
return
|
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
|
311
|
-
return
|
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
|
323
|
-
return
|
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
|
2
|
+
pragma solidity >=0.8.4;
|
3
3
|
|
4
4
|
import "./InitializableAccessControl.sol";
|
5
|
-
import "
|
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,
|
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
|
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
package/index.js
CHANGED
@@ -9,6 +9,9 @@ const {
|
|
9
9
|
not,
|
10
10
|
} = require("./scripts/include/features_roles");
|
11
11
|
|
12
|
+
// RBAC behaviours
|
13
|
+
const {behavesLikeRBAC} = require("./test/include/rbac.behaviour");
|
14
|
+
|
12
15
|
// Re-export the functions
|
13
16
|
module.exports = {
|
14
17
|
ROLE_ACCESS_MANAGER,
|
@@ -16,4 +19,5 @@ module.exports = {
|
|
16
19
|
FULL_PRIVILEGES_MASK,
|
17
20
|
or,
|
18
21
|
not,
|
22
|
+
behavesLikeRBAC,
|
19
23
|
};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lazy-sol/access-control-upgradeable",
|
3
|
-
"version": "1.0
|
3
|
+
"version": "1.1.0",
|
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": {
|
@@ -11,7 +11,7 @@
|
|
11
11
|
"deploy": "hardhat deploy"
|
12
12
|
},
|
13
13
|
"engines": {
|
14
|
-
"node": ">=
|
14
|
+
"node": ">=18.0.0"
|
15
15
|
},
|
16
16
|
"keywords": [
|
17
17
|
"RBAC",
|
@@ -25,19 +25,22 @@
|
|
25
25
|
"@lazy-sol/a-missing-gem": "^1.0.9",
|
26
26
|
"@nomiclabs/hardhat-truffle5": "^2.0.7",
|
27
27
|
"@openzeppelin/contracts-upgradeable": "^4.9.6",
|
28
|
-
"
|
29
|
-
"hardhat": "^2.22.6",
|
28
|
+
"hardhat": "^2.22.9",
|
30
29
|
"hardhat-deploy": "^0.11.45"
|
31
30
|
},
|
32
31
|
"devDependencies": {
|
33
|
-
"@lazy-sol/zeppelin-test-helpers": "^1.0.
|
32
|
+
"@lazy-sol/zeppelin-test-helpers": "^1.0.5",
|
34
33
|
"@openzeppelin/contracts": "^4.9.6",
|
35
34
|
"hardhat-dependency-injector": "^1.0.1",
|
36
35
|
"hardhat-gas-reporter": "^1.0.10",
|
37
|
-
"solidity-coverage": "^0.8.
|
36
|
+
"solidity-coverage": "^0.8.13"
|
38
37
|
},
|
39
38
|
"overrides": {
|
39
|
+
"axios": ">=1.7.5",
|
40
|
+
"micromatch": "^4.0.8",
|
41
|
+
"tar": "^6.2.1",
|
40
42
|
"tough-cookie": "^4.1.3",
|
41
|
-
"yargs-parser": "^5.0.1"
|
43
|
+
"yargs-parser": "^5.0.1",
|
44
|
+
"ws": "^8.0.0"
|
42
45
|
}
|
43
46
|
}
|
@@ -24,7 +24,8 @@ const {
|
|
24
24
|
// RBAC core features and roles
|
25
25
|
const {
|
26
26
|
not,
|
27
|
-
ROLE_ACCESS_MANAGER,
|
27
|
+
ROLE_ACCESS_MANAGER,
|
28
|
+
FULL_PRIVILEGES_MASK,
|
28
29
|
} = require("../../scripts/include/features_roles");
|
29
30
|
|
30
31
|
/**
|
@@ -40,6 +41,18 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
40
41
|
const by = a1;
|
41
42
|
const to = a2;
|
42
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
|
+
});
|
43
56
|
describe("deployment and initial state", function() {
|
44
57
|
function deploy_and_check(owner, features) {
|
45
58
|
let access_control;
|
@@ -81,7 +94,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
81
94
|
deploy_and_check(a1, random_bn256());
|
82
95
|
});
|
83
96
|
});
|
84
|
-
describe("when deployed with
|
97
|
+
describe("when deployed with no initial features", function() {
|
85
98
|
let access_control;
|
86
99
|
beforeEach(async function() {
|
87
100
|
access_control = await deployment_fn.call(this, a0);
|
@@ -92,7 +105,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
92
105
|
beforeEach(async function() {
|
93
106
|
await access_control.updateRole(by, ROLE_ACCESS_MANAGER, {from: a0});
|
94
107
|
});
|
95
|
-
describe("when ACCESS_MANAGER has full set of permissions", function() {
|
108
|
+
describe("when ACCESS_MANAGER has the full set of permissions", function() {
|
96
109
|
beforeEach(async function() {
|
97
110
|
await access_control.updateRole(by, MAX_UINT256, {from: a0});
|
98
111
|
});
|
@@ -271,6 +284,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
271
284
|
it("operator becomes an ACCESS_MANAGER", async function() {
|
272
285
|
expect(await access_control.isOperatorInRole(to, ROLE_ACCESS_MANAGER), "operator").to.be.true;
|
273
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});
|
274
288
|
});
|
275
289
|
});
|
276
290
|
describe("when ACCESS_MANAGER revokes ACCESS_MANAGER permission from itself", function() {
|
@@ -280,15 +294,16 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
280
294
|
it("operator ceases to be an ACCESS_MANAGER", async function() {
|
281
295
|
expect(await access_control.isOperatorInRole(by, ROLE_ACCESS_MANAGER), "operator").to.be.false;
|
282
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()");
|
283
298
|
});
|
284
299
|
});
|
285
300
|
});
|
286
301
|
describe("otherwise (no ACCESS_MANAGER permission)", function() {
|
287
302
|
it("updateFeatures reverts", async function() {
|
288
|
-
await expectRevert(access_control.updateFeatures(1, {from: by}), "
|
303
|
+
await expectRevert(access_control.updateFeatures(1, {from: by}), "AccessDenied()");
|
289
304
|
});
|
290
305
|
it("updateRole reverts", async function() {
|
291
|
-
await expectRevert(access_control.updateRole(to, 1, {from: by}), "
|
306
|
+
await expectRevert(access_control.updateRole(to, 1, {from: by}), "AccessDenied()");
|
292
307
|
});
|
293
308
|
});
|
294
309
|
}
|
package/test/rbac_modifier.js
CHANGED
@@ -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}), "
|
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;
|
package/test/rbac_upgradeable.js
CHANGED
@@ -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}), "
|
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");
|