@lazy-sol/access-control-upgradeable 1.1.1 → 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,9 @@
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
+
5
+ v1.1.2: do not enable full privileges to zero address on contract initialization
6
+
1
7
  v1.1.0: Contact Size Optimizations
2
8
 
3
9
  - __Breaking Change:__ Solidity 0.8.4 is now required to compile the contracts (previously was 0.8.2).
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
@@ -174,8 +175,11 @@ abstract contract InitializableAccessControlCore is Initializable {
174
175
  * @param _features initial features mask of the contract, can be zero
175
176
  */
176
177
  function _postConstruct(address _owner, uint256 _features) internal virtual onlyInitializing {
177
- // grant owner full privileges
178
- __setRole(_owner, FULL_PRIVILEGES_MASK, FULL_PRIVILEGES_MASK);
178
+ // if there is a request to set owner (zero address owner means no owner)
179
+ if(_owner != address(0)) {
180
+ // grant owner full privileges
181
+ __setRole(_owner, FULL_PRIVILEGES_MASK, FULL_PRIVILEGES_MASK);
182
+ }
179
183
  // update initial features bitmask
180
184
  __setRole(address(this), _features, _features);
181
185
  }
@@ -199,7 +203,7 @@ abstract contract InitializableAccessControlCore is Initializable {
199
203
  *
200
204
  * @return 256-bit bitmask of the features enabled
201
205
  */
202
- function features() public view returns (uint256) {
206
+ function features() public view returns(uint256) {
203
207
  // features are stored in 'this' address mapping of `userRoles`
204
208
  return getRole(address(this));
205
209
  }
@@ -213,9 +217,9 @@ abstract contract InitializableAccessControlCore is Initializable {
213
217
  *
214
218
  * @param _mask bitmask representing a set of features to enable/disable
215
219
  */
216
- function updateFeatures(uint256 _mask) public {
217
- // delegate call to `updateRole`
218
- updateRole(address(this), _mask);
220
+ function updateFeatures(uint256 _mask) external {
221
+ // delegate to internal `_updateRole()`
222
+ _updateRole(address(this), _mask);
219
223
  }
220
224
 
221
225
  /**
@@ -240,22 +244,34 @@ abstract contract InitializableAccessControlCore is Initializable {
240
244
  * @notice Updates set of permissions (role) for a given user,
241
245
  * taking into account sender's permissions.
242
246
  *
243
- * @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
244
248
  * @dev Setting role to `FULL_PRIVILEGES_MASK` is equivalent to
245
249
  * copying senders' permissions (role) to the user
246
250
  * @dev Requires transaction sender to have `ROLE_ACCESS_MANAGER` permission
247
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
+ *
248
267
  * @param operator address of a user to alter permissions for,
249
268
  * or self address to alter global features of the smart contract
250
269
  * @param role bitmask representing a set of permissions to
251
270
  * enable/disable for a user specified
252
271
  */
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));
272
+ function updateRole(address operator, uint256 role) external {
273
+ // delegate to internal `_updateRole()`
274
+ _updateRole(operator, role);
259
275
  }
260
276
 
261
277
  /**
@@ -367,6 +383,28 @@ abstract contract InitializableAccessControlCore is Initializable {
367
383
  return __hasRole(getRole(operator), required);
368
384
  }
369
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
+
370
408
  /**
371
409
  * @dev Sets the `assignedRole` role to the operator, logs both `requestedRole` and `actualRole`
372
410
  *
@@ -387,7 +425,7 @@ abstract contract InitializableAccessControlCore is Initializable {
387
425
  userRoles[operator] = assignedRole;
388
426
 
389
427
  // fire an event
390
- emit RoleUpdated(operator, requestedRole, assignedRole);
428
+ emit RoleUpdated(msg.sender, operator, requestedRole, assignedRole);
391
429
  }
392
430
 
393
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.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.10",
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",
@@ -59,23 +59,34 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
59
59
  beforeEach(async function() {
60
60
  access_control = await deployment_fn.call(this, a0, owner, features);
61
61
  });
62
- it('"RoleUpdated(owner)" event is emitted correctly', async function() {
63
- await expectEvent.inConstruction(access_control, "RoleUpdated", {
64
- operator: owner,
65
- requested: FULL_PRIVILEGES_MASK,
66
- assigned: FULL_PRIVILEGES_MASK,
62
+ if(owner !== ZERO_ADDRESS) {
63
+ it('"RoleUpdated(owner)" event is emitted correctly', async function() {
64
+ await expectEvent.inConstruction(access_control, "RoleUpdated", {
65
+ by: a0,
66
+ operator: owner,
67
+ requested: FULL_PRIVILEGES_MASK,
68
+ assigned: FULL_PRIVILEGES_MASK,
69
+ });
67
70
  });
68
- });
71
+ }
69
72
  it('"RoleUpdated(this)" event is emitted correctly', async function() {
70
73
  await expectEvent.inConstruction(access_control, "RoleUpdated", {
74
+ by: a0,
71
75
  operator: access_control.address,
72
76
  requested: features,
73
77
  assigned: features,
74
78
  });
75
79
  });
76
- it("owners' role is set correctly", async function() {
77
- expect(await access_control.getRole(owner)).to.be.bignumber.that.equals(FULL_PRIVILEGES_MASK);
78
- });
80
+ if(owner !== ZERO_ADDRESS) {
81
+ it("owners' role is set correctly", async function() {
82
+ expect(await access_control.getRole(owner)).to.be.bignumber.that.equals(FULL_PRIVILEGES_MASK);
83
+ });
84
+ }
85
+ else {
86
+ it("owners' role is not set", async function() {
87
+ expect(await access_control.getRole(owner)).to.be.bignumber.that.equals("0");
88
+ });
89
+ }
79
90
  it("features are set correctly", async function() {
80
91
  expect(await access_control.features()).to.be.bignumber.that.equals(features);
81
92
  });
@@ -125,6 +136,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
125
136
  });
126
137
  it('"RoleUpdated" event', async function() {
127
138
  expectEvent(receipt, "RoleUpdated", {
139
+ by,
128
140
  operator: to_fn(to),
129
141
  requested: set,
130
142
  assigned: set,
@@ -148,6 +160,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
148
160
  });
149
161
  it('"RoleUpdated" event', async function() {
150
162
  expectEvent(receipt, "RoleUpdated", {
163
+ by,
151
164
  operator: to_fn(to),
152
165
  requested: not(remove),
153
166
  assigned: not(remove),
@@ -173,6 +186,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
173
186
  });
174
187
  it('"RoleUpdated" event', async function() {
175
188
  expectEvent(receipt, "RoleUpdated", {
189
+ by,
176
190
  operator: to_fn(to),
177
191
  requested: set,
178
192
  assigned: "0",
@@ -197,6 +211,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
197
211
  });
198
212
  it('"RoleUpdated" event', async function() {
199
213
  expectEvent(receipt, "RoleUpdated", {
214
+ by,
200
215
  operator: to_fn(to),
201
216
  requested: not(remove),
202
217
  assigned: MAX_UINT256,
@@ -228,6 +243,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
228
243
  });
229
244
  it('"RoleUpdated" event', async function() {
230
245
  expectEvent(receipt, "RoleUpdated", {
246
+ by,
231
247
  operator: to_fn(to),
232
248
  requested: set,
233
249
  assigned: role.and(set),
@@ -252,6 +268,7 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
252
268
  });
253
269
  it('"RoleUpdated" event', async function() {
254
270
  expectEvent(receipt, "RoleUpdated", {
271
+ by,
255
272
  operator: to_fn(to),
256
273
  requested: not(remove),
257
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() {