@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 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
@@ -1,6 +1,7 @@
1
1
  The MIT License (MIT)
2
2
 
3
3
  Copyright (c) 2017-2024 Basil Gorin
4
+ Copyright (c) 2024–2025 Lazy So[u]l
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining
6
7
  a copy of this software and associated documentation files (the
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
- (c) 2017–2024 Basil Gorin
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) public view returns (bool) {
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) public view returns (bool) {
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) public view returns (bool) {
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 (uint256) {
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) public {
220
- // delegate call to `updateRole`
221
- updateRole(address(this), _mask);
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 an all permissions
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) public {
257
- // caller must have a permission to update user roles
258
- _requireSenderInRole(ROLE_ACCESS_MANAGER);
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() public view virtual returns (address) {
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
@@ -50,7 +50,7 @@ module.exports = {
50
50
  // https://hardhat.org/guides/compile-contracts.html
51
51
  compilers: [
52
52
  {
53
- version: "0.8.4",
53
+ version: "0.8.28",
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.1.2",
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.11",
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.22.13",
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.13"
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)),
@@ -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() {