@lazy-sol/access-control-upgradeable 1.1.2 → 1.1.4
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 +4 -0
- package/LICENSE.txt +1 -0
- package/README.md +21 -1
- package/audits/1.1_Prem_resolution.md +46 -0
- package/audits/1.1_final_Prem.pdf +0 -0
- package/contracts/InitializableAccessControl.sol +3 -3
- package/contracts/InitializableAccessControlCore.sol +48 -13
- package/contracts/UpgradeableAccessControlCore.sol +1 -1
- package/contracts/mocks/UpgradeableAccessControlMocks.sol +8 -0
- package/hardhat.config.js +1 -1
- package/package.json +6 -4
- package/test/include/rbac.behaviour.js +8 -0
- package/test/rbac_upgradeable.js +3 -0
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
v1.1.3: Prem's audit and its resolution
|
2
|
+
- See the list of issues found and resolved in [the audit resolution doc](./audits/1.1_Prem_resolution.md)
|
3
|
+
- See the audit methodology and issues found in [the original audit report](./audits/1.1_final_Prem.pdf)
|
4
|
+
|
1
5
|
v1.1.2: do not enable full privileges to zero address on contract initialization
|
2
6
|
|
3
7
|
v1.1.0: Contact Size Optimizations
|
package/LICENSE.txt
CHANGED
package/README.md
CHANGED
@@ -7,6 +7,10 @@ A shortcut to a modular and easily pluggable dapp architecture.
|
|
7
7
|
Enable the modular plug and play (PnP) architecture for your dapp by incorporating the role-based access control (RBAC)
|
8
8
|
into the smart contracts.
|
9
9
|
|
10
|
+
## Audit(s)
|
11
|
+
* [v1.1 Audit by Prem, January 1–30, 2025](./audits/1.1_final_Prem.pdf)
|
12
|
+
* [Resolution: v1.1 Audit by Prem](./audits/1.1_Prem_resolution.md)
|
13
|
+
|
10
14
|
## Technical Overview
|
11
15
|
|
12
16
|
Role-based Access Control (RBAC), or simply Access Control, is the base parent contract to be inherited by other smart
|
@@ -219,7 +223,23 @@ Examples:
|
|
219
223
|
[ERC20v1](https://raw.githubusercontent.com/vgorin/solidity-template/master/contracts/token/upgradeable/ERC20v1.sol),
|
220
224
|
[ERC721v1](https://raw.githubusercontent.com/vgorin/solidity-template/master/contracts/token/upgradeable/ERC721v1.sol).
|
221
225
|
|
226
|
+
## Evaluating Currently Enabled Features and Roles on the Deployed Contract
|
227
|
+
|
228
|
+
1. To evaluate currently enabled features use
|
229
|
+
* `features()` function, or
|
230
|
+
* `getRole(this)` function, replacing `this` with the deployed contract address
|
231
|
+
2. To evaluate currently enabled permissions for a **particular** address use
|
232
|
+
* `getRole(address)` function
|
233
|
+
3. To find **all** the addresses having any permissions, track the `RoleUpdated()` event and evaluate the history
|
234
|
+
of `assiged` roles for every `operator` address
|
235
|
+
* Alternatively, use the [tool](ui.html) which automates the process
|
236
|
+
(see demo [here](https://lazy-sol.github.io/access-control/ui.html))
|
237
|
+
|
222
238
|
## See Also
|
223
239
|
[Role-based Access Control (RBAC)](https://github.com/lazy-sol/access-control/blob/master/README.md)
|
224
240
|
|
225
|
-
|
241
|
+
## Contributing
|
242
|
+
Please see the [Contribution Guide](https://github.com/lazy-sol/access-control/blob/master/CONTRIBUTING.md) document to get understanding on how to report issues,
|
243
|
+
contribute to the source code, fix bugs, introduce new features, etc.
|
244
|
+
|
245
|
+
(c) 2017–2025 Basil Gorin
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# AccessControlUpgradable: Smart Contract Audit Report Resolution #
|
2
|
+
|
3
|
+
## Resolution Summary ##
|
4
|
+
|
5
|
+
| ID | | Resolution |
|
6
|
+
|---------|-------------------------------------------------------------|--------------|
|
7
|
+
| Minor-1 | Insufficient Event Logging in `RoleUpdated` | Fixed |
|
8
|
+
| Minor-2 | No Check for `role != 0` in `updateRole` | Mitigated |
|
9
|
+
| Notes-1 | Version Constraints with Known Issues | Mitigated |
|
10
|
+
| Notes-2 | Visibility Optimization for Gas Savings | Fixed |
|
11
|
+
| Notes-3 | Potential Improvement for `FULL_PRIVILEGES_MASK` Assignment | Acknowledged |
|
12
|
+
| Notes-4 | Test Coverage | Fixed |
|
13
|
+
|
14
|
+
For issues which were ignored, acknowledged, mitigated, or fixed differently than suggested by the auditor, see the
|
15
|
+
[Comments](#comments) section below.
|
16
|
+
|
17
|
+
## Comments ##
|
18
|
+
|
19
|
+
### Minor-2. No Check for `role != 0` in `updateRole` ###
|
20
|
+
To be used as a parent contract for RBAC-based applications, the `AccessControl` contract is originally designed to be
|
21
|
+
lightweight, which was improved even further in version 1.1 by introducing the `AccessControlCore` contract.
|
22
|
+
|
23
|
+
To minimise contract size, we minimise the number of public functions exposed and use a single public function
|
24
|
+
`updateRole` to add, modify, and delete permissions, including self-revoke.
|
25
|
+
|
26
|
+
Mitigated by adding an explicit and very well noticeable comment in the SolDoc for `updateRole` function.
|
27
|
+
|
28
|
+
### Notes-1. Version Constraints with Known Issues ###
|
29
|
+
To allow the use as a parent contract for a wide range of RBAC-based applications, and serve as a Solidity library,
|
30
|
+
we try to keep pragma constraint as low as possible. This approach maximizes compatibility.
|
31
|
+
|
32
|
+
Mitigated by updating the compiler version to 0.8.28 in `hardhat.config.js`.
|
33
|
+
|
34
|
+
### Notes-2. Visibility Optimization for Gas Savings
|
35
|
+
Visibility modifier was changed from `public` to `external` for functions `isFeatureEnabled`, `isSenderInRole`,
|
36
|
+
`isOperatorInRole`, `updateFeatures`, `updateRole`, `updateAccessRole`, and `deployNewOwnableToAccessControlAdapter`.
|
37
|
+
|
38
|
+
Functions `features`, and `getRole` remain public to be accessible in inheriting contracts.
|
39
|
+
|
40
|
+
### Notes-3. Potential Improvement for `FULL_PRIVILEGES_MASK` Assignment ###
|
41
|
+
Role-based Access Control (RBAC) library, and its AccessControl* contracts are designed to be a long-term, but still
|
42
|
+
temporary solution for the projects evolving in the direction of the fully decentralized operation. RBAC Lifecycle
|
43
|
+
assumes that all the permissions are eventually either fully revoked from external participants, or are fully
|
44
|
+
transitioned to the DAO governance smart contract.
|
45
|
+
|
46
|
+
Thus, further improvements to `FULL_PRIVILEGES_MASK` assignments and management are out of scope for the RBAC library.
|
Binary file
|
@@ -67,7 +67,7 @@ abstract contract InitializableAccessControl is InitializableAccessControlCore {
|
|
67
67
|
* @param required set of features to check against
|
68
68
|
* @return true if all the features requested are enabled, false otherwise
|
69
69
|
*/
|
70
|
-
function isFeatureEnabled(uint256 required)
|
70
|
+
function isFeatureEnabled(uint256 required) external view returns (bool) {
|
71
71
|
// delegate to internal `_isFeatureEnabled`
|
72
72
|
return _isFeatureEnabled(required);
|
73
73
|
}
|
@@ -78,7 +78,7 @@ abstract contract InitializableAccessControl is InitializableAccessControlCore {
|
|
78
78
|
* @param required set of permissions (role) to check against
|
79
79
|
* @return true if all the permissions requested are enabled, false otherwise
|
80
80
|
*/
|
81
|
-
function isSenderInRole(uint256 required)
|
81
|
+
function isSenderInRole(uint256 required) external view returns (bool) {
|
82
82
|
// delegate to internal `_isSenderInRole`
|
83
83
|
return _isSenderInRole(required);
|
84
84
|
}
|
@@ -90,7 +90,7 @@ abstract contract InitializableAccessControl is InitializableAccessControlCore {
|
|
90
90
|
* @param required set of permissions (role) to check
|
91
91
|
* @return true if all the permissions requested are enabled, false otherwise
|
92
92
|
*/
|
93
|
-
function isOperatorInRole(address operator, uint256 required)
|
93
|
+
function isOperatorInRole(address operator, uint256 required) external view returns (bool) {
|
94
94
|
// delegate to internal `_isOperatorInRole`
|
95
95
|
return _isOperatorInRole(operator, required);
|
96
96
|
}
|
@@ -130,11 +130,12 @@ abstract contract InitializableAccessControlCore is Initializable {
|
|
130
130
|
/**
|
131
131
|
* @dev Fired in updateRole() and updateFeatures()
|
132
132
|
*
|
133
|
+
* @param by address which has granted/revoked permissions to operator
|
133
134
|
* @param operator address which was granted/revoked permissions
|
134
135
|
* @param requested permissions requested
|
135
136
|
* @param assigned permissions effectively set
|
136
137
|
*/
|
137
|
-
event RoleUpdated(address indexed operator, uint256 requested, uint256 assigned);
|
138
|
+
event RoleUpdated(address indexed by, address indexed operator, uint256 requested, uint256 assigned);
|
138
139
|
|
139
140
|
/**
|
140
141
|
* @notice Function modifier making a function defined as public behave as restricted
|
@@ -202,7 +203,7 @@ abstract contract InitializableAccessControlCore is Initializable {
|
|
202
203
|
*
|
203
204
|
* @return 256-bit bitmask of the features enabled
|
204
205
|
*/
|
205
|
-
function features() public view returns
|
206
|
+
function features() public view returns(uint256) {
|
206
207
|
// features are stored in 'this' address mapping of `userRoles`
|
207
208
|
return getRole(address(this));
|
208
209
|
}
|
@@ -216,9 +217,9 @@ abstract contract InitializableAccessControlCore is Initializable {
|
|
216
217
|
*
|
217
218
|
* @param _mask bitmask representing a set of features to enable/disable
|
218
219
|
*/
|
219
|
-
function updateFeatures(uint256 _mask)
|
220
|
-
// delegate
|
221
|
-
|
220
|
+
function updateFeatures(uint256 _mask) external {
|
221
|
+
// delegate to internal `_updateRole()`
|
222
|
+
_updateRole(address(this), _mask);
|
222
223
|
}
|
223
224
|
|
224
225
|
/**
|
@@ -243,22 +244,34 @@ abstract contract InitializableAccessControlCore is Initializable {
|
|
243
244
|
* @notice Updates set of permissions (role) for a given user,
|
244
245
|
* taking into account sender's permissions.
|
245
246
|
*
|
246
|
-
* @dev Setting role to zero is equivalent to removing
|
247
|
+
* @dev Setting role to zero is equivalent to removing all the permissions
|
247
248
|
* @dev Setting role to `FULL_PRIVILEGES_MASK` is equivalent to
|
248
249
|
* copying senders' permissions (role) to the user
|
249
250
|
* @dev Requires transaction sender to have `ROLE_ACCESS_MANAGER` permission
|
250
251
|
*
|
252
|
+
* ╔════════════════════════════════════════════════════════════════════════╗
|
253
|
+
* ║ WARNING: RISK OF ACCIDENTAL SELF-REVOKE ║
|
254
|
+
* ╠════════════════════════════════════════════════════════════════════════╣
|
255
|
+
* ║ updateRole function is used to add, update, delete permissions, to ║
|
256
|
+
* ║ revoke and self-revoke all the permissions, as well as to disable ║
|
257
|
+
* ║ contract upgradability by revoking ROLE_UPGRADE_MANAGER permission ║
|
258
|
+
* ║ ║
|
259
|
+
* ║ updateRole(msg.sender, 0) executed by a super admin themselves ║
|
260
|
+
* ║ revokes super admin permissions forever if there is no other super ║
|
261
|
+
* ║ admin set prior to updateRole(msg.sender, 0) call ║
|
262
|
+
* ║ ║
|
263
|
+
* ║ Note that in such a case upgradeable contracts also stop being ║
|
264
|
+
* ║ upgradable as ROLE_UPGRADE_MANAGER permission is revoked ║
|
265
|
+
* ╚════════════════════════════════════════════════════════════════════════╝
|
266
|
+
*
|
251
267
|
* @param operator address of a user to alter permissions for,
|
252
268
|
* or self address to alter global features of the smart contract
|
253
269
|
* @param role bitmask representing a set of permissions to
|
254
270
|
* enable/disable for a user specified
|
255
271
|
*/
|
256
|
-
function updateRole(address operator, uint256 role)
|
257
|
-
//
|
258
|
-
|
259
|
-
|
260
|
-
// evaluate the role and reassign it
|
261
|
-
__setRole(operator, role, _evaluateBy(msg.sender, getRole(operator), role));
|
272
|
+
function updateRole(address operator, uint256 role) external {
|
273
|
+
// delegate to internal `_updateRole()`
|
274
|
+
_updateRole(operator, role);
|
262
275
|
}
|
263
276
|
|
264
277
|
/**
|
@@ -370,6 +383,28 @@ abstract contract InitializableAccessControlCore is Initializable {
|
|
370
383
|
return __hasRole(getRole(operator), required);
|
371
384
|
}
|
372
385
|
|
386
|
+
/**
|
387
|
+
* @dev Updates set of permissions (role) for a given user,
|
388
|
+
* taking into account sender's permissions.
|
389
|
+
*
|
390
|
+
* @dev Setting role to zero is equivalent to removing all the permissions
|
391
|
+
* @dev Setting role to `FULL_PRIVILEGES_MASK` is equivalent to
|
392
|
+
* copying senders' permissions (role) to the user
|
393
|
+
* @dev Requires transaction sender to have `ROLE_ACCESS_MANAGER` permission
|
394
|
+
*
|
395
|
+
* @param operator address of a user to alter permissions for,
|
396
|
+
* or self address to alter global features of the smart contract
|
397
|
+
* @param role bitmask representing a set of permissions to
|
398
|
+
* enable/disable for a user specified
|
399
|
+
*/
|
400
|
+
function _updateRole(address operator, uint256 role) internal {
|
401
|
+
// caller must have a permission to update user roles
|
402
|
+
_requireSenderInRole(ROLE_ACCESS_MANAGER);
|
403
|
+
|
404
|
+
// evaluate the role and reassign it
|
405
|
+
__setRole(operator, role, _evaluateBy(msg.sender, getRole(operator), role));
|
406
|
+
}
|
407
|
+
|
373
408
|
/**
|
374
409
|
* @dev Sets the `assignedRole` role to the operator, logs both `requestedRole` and `actualRole`
|
375
410
|
*
|
@@ -390,7 +425,7 @@ abstract contract InitializableAccessControlCore is Initializable {
|
|
390
425
|
userRoles[operator] = assignedRole;
|
391
426
|
|
392
427
|
// fire an event
|
393
|
-
emit RoleUpdated(operator, requestedRole, assignedRole);
|
428
|
+
emit RoleUpdated(msg.sender, operator, requestedRole, assignedRole);
|
394
429
|
}
|
395
430
|
|
396
431
|
/**
|
@@ -73,7 +73,7 @@ abstract contract UpgradeableAccessControlCore is InitializableAccessControlCore
|
|
73
73
|
*
|
74
74
|
* @return the current implementation address
|
75
75
|
*/
|
76
|
-
function getImplementation()
|
76
|
+
function getImplementation() external view virtual returns (address) {
|
77
77
|
// delegate to `ERC1967Upgrade._getImplementation()`
|
78
78
|
return _getImplementation();
|
79
79
|
}
|
@@ -22,6 +22,10 @@ contract UpgradeableAccessControl1 is UpgradeableAccessControlMock {
|
|
22
22
|
string public version1;
|
23
23
|
|
24
24
|
function postConstruct(address _owner, uint256 _features) public virtual initializer {
|
25
|
+
postConstructNonInit(_owner, _features);
|
26
|
+
}
|
27
|
+
|
28
|
+
function postConstructNonInit(address _owner, uint256 _features) public virtual {
|
25
29
|
_postConstruct(_owner, _features);
|
26
30
|
version1 = "1";
|
27
31
|
}
|
@@ -31,6 +35,10 @@ contract UpgradeableAccessControl2 is UpgradeableAccessControlMock {
|
|
31
35
|
string public version2;
|
32
36
|
|
33
37
|
function postConstruct(address _owner, uint256 _features) public virtual initializer {
|
38
|
+
postConstructNonInit(_owner, _features);
|
39
|
+
}
|
40
|
+
|
41
|
+
function postConstructNonInit(address _owner, uint256 _features) public virtual {
|
34
42
|
_postConstruct(_owner, _features);
|
35
43
|
version2 = "2";
|
36
44
|
}
|
package/hardhat.config.js
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lazy-sol/access-control-upgradeable",
|
3
|
-
"version": "1.1.
|
3
|
+
"version": "1.1.4",
|
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": {
|
@@ -25,17 +25,19 @@
|
|
25
25
|
"@openzeppelin/contracts-upgradeable": "4.9.6"
|
26
26
|
},
|
27
27
|
"devDependencies": {
|
28
|
-
"@lazy-sol/a-missing-gem": "^1.0.
|
28
|
+
"@lazy-sol/a-missing-gem": "^1.0.12",
|
29
29
|
"@lazy-sol/zeppelin-test-helpers": "^1.0.5",
|
30
30
|
"@nomiclabs/hardhat-truffle5": "^2.0.7",
|
31
31
|
"@openzeppelin/contracts": "4.9.6",
|
32
|
-
"hardhat": "^2.
|
32
|
+
"hardhat": "^2.25.0",
|
33
33
|
"hardhat-dependency-injector": "^1.0.1",
|
34
34
|
"hardhat-gas-reporter": "^1.0.10",
|
35
|
-
"solidity-coverage": "^0.8.
|
35
|
+
"solidity-coverage": "^0.8.16"
|
36
36
|
},
|
37
37
|
"overrides": {
|
38
38
|
"axios": ">=1.7.5",
|
39
|
+
"cookie": ">=0.7.0",
|
40
|
+
"elliptic": "^6.6.0",
|
39
41
|
"micromatch": "^4.0.8",
|
40
42
|
"tar": "^6.2.1",
|
41
43
|
"tough-cookie": "^4.1.3",
|
@@ -62,6 +62,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
62
62
|
if(owner !== ZERO_ADDRESS) {
|
63
63
|
it('"RoleUpdated(owner)" event is emitted correctly', async function() {
|
64
64
|
await expectEvent.inConstruction(access_control, "RoleUpdated", {
|
65
|
+
by: a0,
|
65
66
|
operator: owner,
|
66
67
|
requested: FULL_PRIVILEGES_MASK,
|
67
68
|
assigned: FULL_PRIVILEGES_MASK,
|
@@ -70,6 +71,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
70
71
|
}
|
71
72
|
it('"RoleUpdated(this)" event is emitted correctly', async function() {
|
72
73
|
await expectEvent.inConstruction(access_control, "RoleUpdated", {
|
74
|
+
by: a0,
|
73
75
|
operator: access_control.address,
|
74
76
|
requested: features,
|
75
77
|
assigned: features,
|
@@ -134,6 +136,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
134
136
|
});
|
135
137
|
it('"RoleUpdated" event', async function() {
|
136
138
|
expectEvent(receipt, "RoleUpdated", {
|
139
|
+
by,
|
137
140
|
operator: to_fn(to),
|
138
141
|
requested: set,
|
139
142
|
assigned: set,
|
@@ -157,6 +160,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
157
160
|
});
|
158
161
|
it('"RoleUpdated" event', async function() {
|
159
162
|
expectEvent(receipt, "RoleUpdated", {
|
163
|
+
by,
|
160
164
|
operator: to_fn(to),
|
161
165
|
requested: not(remove),
|
162
166
|
assigned: not(remove),
|
@@ -182,6 +186,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
182
186
|
});
|
183
187
|
it('"RoleUpdated" event', async function() {
|
184
188
|
expectEvent(receipt, "RoleUpdated", {
|
189
|
+
by,
|
185
190
|
operator: to_fn(to),
|
186
191
|
requested: set,
|
187
192
|
assigned: "0",
|
@@ -206,6 +211,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
206
211
|
});
|
207
212
|
it('"RoleUpdated" event', async function() {
|
208
213
|
expectEvent(receipt, "RoleUpdated", {
|
214
|
+
by,
|
209
215
|
operator: to_fn(to),
|
210
216
|
requested: not(remove),
|
211
217
|
assigned: MAX_UINT256,
|
@@ -237,6 +243,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
237
243
|
});
|
238
244
|
it('"RoleUpdated" event', async function() {
|
239
245
|
expectEvent(receipt, "RoleUpdated", {
|
246
|
+
by,
|
240
247
|
operator: to_fn(to),
|
241
248
|
requested: set,
|
242
249
|
assigned: role.and(set),
|
@@ -261,6 +268,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
|
|
261
268
|
});
|
262
269
|
it('"RoleUpdated" event', async function() {
|
263
270
|
expectEvent(receipt, "RoleUpdated", {
|
271
|
+
by,
|
264
272
|
operator: to_fn(to),
|
265
273
|
requested: not(remove),
|
266
274
|
assigned: not(role.and(remove)),
|
package/test/rbac_upgradeable.js
CHANGED
@@ -54,6 +54,9 @@ contract("UpgradeableAccessControl (U-RBAC) Core tests", function(accounts) {
|
|
54
54
|
it("it is impossible to re-initialize", async function() {
|
55
55
|
await expectRevert(ac.postConstruct(ZERO_ADDRESS, 0, {from: a0}), "Initializable: contract is already initialized");
|
56
56
|
});
|
57
|
+
it("it is impossible to postConstruct non-initializing", async function() {
|
58
|
+
await expectRevert(ac.postConstructNonInit(ZERO_ADDRESS, 0, {from: a0}), "Initializable: contract is not initializing");
|
59
|
+
});
|
57
60
|
describe("when there is new (v2) implementation available", function() {
|
58
61
|
let impl2;
|
59
62
|
beforeEach(async function() {
|