@lazy-sol/access-control 1.1.0 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ v1.1.2: do not enable full privileges to zero address on construction
2
+
3
+ v1.1.1: Role-based Access Control (RBAC) Inspector
4
+ - Introduced the Role-based Access Control (RBAC) Inspector UI capable of evaluating features and roles
5
+ for already deployed contracts; supports all EVM-based networks (mainnet, sepolia, etc.)
6
+ - Optimized npm imports moving all prod dependencies into dev dependencies
7
+
1
8
  v1.1.0: Contact Size Optimizations
2
9
 
3
10
  - __Breaking Change:__ Solidity 0.8.4 is now required to compile the contracts
package/README.md CHANGED
@@ -65,7 +65,7 @@ features and roles. Usually, smart contracts use different values for all the fe
65
65
  next section).
66
66
 
67
67
  Access manager may revoke its own permissions, including the bit 255. Eventually that allows an access manager to let
68
- the smart contract “float freely” and be controlled only by the community (via DAO) or by no one at all.
68
+ the smart contract “float freely” and be controlled only by the community (via the DAO) or by no one at all.
69
69
 
70
70
  ## Comparing with OpenZeppelin
71
71
 
@@ -73,7 +73,7 @@ Both our and OpenZeppelin Access Control implementations feature a similar API t
73
73
  thing".
74
74
 
75
75
  Zeppelin implementation is more flexible:
76
- * it allows setting unlimited number of roles, while current is limited to 256 different roles
76
+ * it allows setting an unlimited number of roles, while current is limited to 256 different roles
77
77
  * it allows setting an admin for each role, while current allows having only one global admin
78
78
 
79
79
  Our implementation is more lightweight:
@@ -93,11 +93,11 @@ npm i -D @lazy-sol/access-control
93
93
  Restricted function is a function with a `public` Solidity modifier access to which is restricted
94
94
  so that only a pre-configured set of accounts can execute it.
95
95
 
96
- 1. Enable role-based access control (RBAC) in a new smart contract
97
- by inheriting the RBAC contract from the [AccessControl](./contracts/AccessControl.sol) contract:
96
+ 1. Enable role-based access control (RBAC) in a new smart contract
97
+ by inheriting the RBAC contract from the [AccessControlCore](./contracts/AccessControlCore.sol) contract:
98
98
  ```solidity
99
99
  import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
100
- import "@lazy-sol/access-control/contracts/AccessControl.sol";
100
+ import "@lazy-sol/access-control/contracts/AccessControlCore.sol";
101
101
 
102
102
  /**
103
103
  * @title Simple ERC20 Implementation
@@ -106,29 +106,29 @@ so that only a pre-configured set of accounts can execute it.
106
106
  *
107
107
  * @author Lazy So[u]l
108
108
  */
109
- contract MyERC20Token is ERC20, AccessControl {
109
+ contract MyERC20Token is ERC20, AccessControlCore {
110
110
 
111
111
  ...
112
112
 
113
113
  }
114
114
  ```
115
115
 
116
- 2. Define an access control role with the unique integer value:
116
+ 2. Define an access control role with the unique integer value:
117
117
  ```solidity
118
118
  ...
119
119
 
120
120
  /**
121
121
  * @notice Token creator is responsible for creating (minting)
122
- tokens to an arbitrary address
122
+ * tokens to an arbitrary address
123
123
  * @dev Role ROLE_TOKEN_CREATOR allows minting tokens
124
- (calling `mint` function)
124
+ * (calling `mint` function)
125
125
  */
126
126
  uint32 public constant ROLE_TOKEN_CREATOR = 0x0001_0000;
127
127
 
128
128
  ...
129
129
  ```
130
130
 
131
- 3. Add the `require(isSenderInRole(ROLE_TOKEN_CREATOR), "access denied")"` check into the function body:
131
+ 3. Add the `_requireSenderInRole(ROLE_TOKEN_CREATOR)` check into the function body:
132
132
  ```solidity
133
133
  ...
134
134
 
@@ -137,7 +137,7 @@ so that only a pre-configured set of accounts can execute it.
137
137
  */
138
138
  function _mint(address _to, uint256 _value) internal virtual override {
139
139
  // check if caller has sufficient permissions to mint tokens
140
- require(isSenderInRole(ROLE_TOKEN_CREATOR), "access denied");
140
+ _requireSenderInRole(ROLE_TOKEN_CREATOR);
141
141
 
142
142
  // delegate to super implementation
143
143
  super._mint(_to, _value);
@@ -146,8 +146,8 @@ so that only a pre-configured set of accounts can execute it.
146
146
  ...
147
147
  ```
148
148
 
149
- Note: you could also use the `restrictedTo` modifier in the function declaration instead of the `require`
150
- in the function body if you don't need a custom error message:
149
+ Note: it is also possible to use the `restrictedTo` modifier in the function declaration instead of the `require`
150
+ in the function body if this better suits the coding style:
151
151
  ```solidity
152
152
  ...
153
153
 
@@ -162,6 +162,31 @@ so that only a pre-configured set of accounts can execute it.
162
162
  ...
163
163
  ```
164
164
 
165
+ ### Customizing the Error Message
166
+
167
+ Modifier `restrictedTo()`, internal functions `_requireSenderInRole()` and `_requireAccessCondition()` throw the
168
+ `AccessDenied` denied error when access check fails.
169
+
170
+ It is also possible to use your own custom errors or string messages if needed by leveraging a lower level boolean
171
+ functions `_isSenderInRole()` and `_isOperatorInRole()`:
172
+
173
+ ```solidity
174
+ ...
175
+
176
+ /**
177
+ * @inheritdoc ERC20
178
+ */
179
+ function _mint(address _to, uint256 _value) internal virtual override {
180
+ // check if caller has sufficient permissions to mint tokens
181
+ require(_isSenderInRole(ROLE_TOKEN_CREATOR), "access denied");
182
+
183
+ // delegate to super implementation
184
+ super._mint(_to, _value);
185
+ }
186
+
187
+ ...
188
+ ```
189
+
165
190
  Examples:
166
191
  [ERC20Impl](https://raw.githubusercontent.com/vgorin/solidity-template/master/contracts/token/ERC20Impl.sol),
167
192
  [ERC721Impl](https://raw.githubusercontent.com/vgorin/solidity-template/master/contracts/token/ERC721Impl.sol).
@@ -177,32 +202,32 @@ Many deployed contracts are using this model, and when the time comes to switch
177
202
 
178
203
  Prerequisite: deployed OZ Ownable contract (target contract) address (target_address)
179
204
 
180
- 1. Deploy the AccessControl Adapter bound to the already deployed OZ Ownable contract
181
- (specify the target OZ Ownable contract address in the constructor upon the deployment)
205
+ 1. Deploy the AccessControl Adapter bound to the already deployed OZ Ownable contract
206
+ (specify the target OZ Ownable contract address in the constructor upon the deployment)
182
207
  ```javascript
183
208
  const adapter = await (artifacts.require("OwnableToAccessControlAdapter")).new(target_address);
184
209
  ```
185
210
 
186
- 2. Define what Ownable-restricted public functions on the target contract you'd like to be able
187
- to provide access to through the adapter contract
211
+ 2. Define what Ownable-restricted public functions on the target contract you'd like to be able
212
+ to provide access to through the adapter contract
188
213
 
189
- 3. Map every such function with the role required to execute it using `updateAccessRole()` function
190
- For example, to be able to provide an access to the transferOwnership(address) function, you could do
214
+ 3. Map every such function with the role required to execute it using `updateAccessRole()` function
215
+ For example, to be able to provide an access to the transferOwnership(address) function, you could do
191
216
  ```javascript
192
217
  const ROLE_TRANSFER_OWNERSHIP_MANAGER = 0x00010000;
193
218
  await adapter.updateAccessRole("transferOwnership(address)", ROLE_TRANSFER_OWNERSHIP_MANAGER);
194
- ```
219
+ ```
195
220
 
196
- 4. Provide the roles to the corresponding operators as you would usually do with AccessControl
197
- For example, if you wish an address 0x00000000000000000000000000000000000Ff1CE to grant an access to the
198
- transferOwnership(address) function on the target, you could do
221
+ 4. Provide the roles to the corresponding operators as you would usually do with AccessControl
222
+ For example, if you wish an address 0x00000000000000000000000000000000000Ff1CE to grant an access to the
223
+ transferOwnership(address) function on the target, you could do
199
224
  ```javascript
200
225
  const operator = "0x00000000000000000000000000000000000Ff1CE";
201
226
  await adapter.updateRole(operator, ROLE_TRANSFER_OWNERSHIP_MANAGER);
202
227
  ```
203
228
 
204
- 5. Transfer the ownership of the target contract to the deployed AccessControl Adapter contract
205
- Note that you can also do steps 2-4 after the step 5
229
+ 5. Transfer the ownership of the target contract to the deployed AccessControl Adapter contract
230
+ Note that you can also do steps 2-4 after the step 5
206
231
 
207
232
  #### Usage Flow
208
233
 
@@ -210,16 +235,16 @@ Prerequisite: installed AccessControl Adapter with the access to at least one re
210
235
  function configured
211
236
 
212
237
  To execute the restricted access function on the target contract via the AccessControl Adapter
213
- 1. Use target contract ABI to construct a low-level function call calldata
214
- For example, to construct the transferOwnership() function calldata to transfer the ownership to the
215
- 0x00000000000000000000000000000000DEAdc0De address, you could do
238
+ 1. Use target contract ABI to construct a low-level function call calldata
239
+ For example, to construct the transferOwnership() function calldata to transfer the ownership to the
240
+ 0x00000000000000000000000000000000DEAdc0De address, you could do
216
241
  ```javascript
217
242
  const to = "0x00000000000000000000000000000000DEAdc0De";
218
243
  const calldata = target.contract.methods.transferOwnership(to).encodeABI();
219
244
  ```
220
245
 
221
- 2. Execute a low-level function call on the AccessControl Adapter contract using the constructed calldata
222
- For example, to execute the transferOwnership() function (prepared in step 1), you could do
246
+ 2. Execute a low-level function call on the AccessControl Adapter contract using the constructed calldata
247
+ For example, to execute the transferOwnership() function (prepared in step 1), you could do
223
248
  ```javascript
224
249
  await web3.eth.sendTransaction({
225
250
  from: operator,
@@ -228,9 +253,20 @@ To execute the restricted access function on the target contract via the AccessC
228
253
  }
229
254
  ```
230
255
 
231
- 3. It is also ok to add an ether to the transaction by adding a value field to the `sendTransaction` call,
232
- as well as sending plain ether transfer transaction, as long as target contract has payable functions,
233
- and/or has a default payable receiver
256
+ 3. It is also ok to add an ether to the transaction by adding a value field to the `sendTransaction` call,
257
+ as well as sending plain ether transfer transaction, as long as target contract has payable functions,
258
+ and/or has a default payable receiver
259
+
260
+ ## Evaluating Currently Enabled Features and Roles on the Deployed Contract
261
+
262
+ 1. To evaluate currently enabled features use
263
+ * `features()` function, or
264
+ * `getRole(this)` function, replacing `this` with the deployed contract address
265
+ 2. To evaluate currently enabled permissions for a **particular** address use
266
+ * `getRole(address)` function
267
+ 3. To find **all** the addresses having any permissions, track the `RoleUpdated()` event and evaluate the history
268
+ of `assiged` roles for every `operator` address
269
+ * Alternatively, use the [tool](ui.html) which automates the process
234
270
 
235
271
  ## Contributing
236
272
  Please see the [Contribution Guide](./CONTRIBUTING.md) document to get understanding on how to report issues,
@@ -278,8 +278,8 @@
278
278
  "type": "receive"
279
279
  }
280
280
  ],
281
- "bytecode": "0x608060405234801561001057600080fd5b50604051610a23380380610a2383398101604081905261002f9161012f565b80600061003f82600019806100bb565b61004a3082806100bb565b50506001600160a01b0382166100955760405162461bcd60e51b815260206004820152600c60248201526b7a65726f206164647265737360a01b604482015260640160405180910390fd5b50600180546001600160a01b0319166001600160a01b0392909216919091179055610162565b6001600160a01b0383166000818152602081815260409182902084905581518581529081018490527fe9be537308880e0f56b7d7cfd7abf85f14c4934486d138f848b92a0cbaf659b4910160405180910390a2505050565b80516001600160a01b038116811461012a57600080fd5b919050565b6000806040838503121561014257600080fd5b61014b83610113565b915061015960208401610113565b90509250929050565b6108b2806101716000396000f3fe6080604052600436106100a05760003560e01c8063491d261111610064578063491d2611146101d35780635defb40d146101f3578063ae5b102e14610213578063ae682e2e14610233578063d4b839921461024b578063d5bb7f6714610283576100bf565b806309c5eabe146100ff5780630e82fe25146101285780632b5214161461016357806334e48c9e14610185578063442767331461019d576100bf565b366100bf576100bd604051806020016040528060008152506102a3565b005b6100bd6000368080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506102a392505050565b61011261010d36600461064c565b6102a3565b60405161011f91906106ed565b60405180910390f35b34801561013457600080fd5b5061015561014336600461071d565b60026020526000908152604090205481565b60405190815260200161011f565b34801561016f57600080fd5b5030600090815260208190526040902054610155565b34801561019157600080fd5b50610155600160fd1b81565b3480156101a957600080fd5b506101556101b836600461074f565b6001600160a01b031660009081526020819052604090205490565b3480156101df57600080fd5b506100bd6101ee36600461076a565b610411565b3480156101ff57600080fd5b506100bd61020e366004610794565b610477565b34801561021f57600080fd5b506100bd61022e3660046107ed565b61048c565b34801561023f57600080fd5b50610155600160ff1b81565b34801561025757600080fd5b5060015461026b906001600160a01b031681565b6040516001600160a01b03909116815260200161011f565b34801561028f57600080fd5b506100bd61029e366004610809565b6104f5565b602081015181516060919015610323576001600160e01b03198116600090815260026020526040812054908190036103185760405162461bcd60e51b81526020600482015260136024820152721858d8d95cdcc81c9bdb19481b9bdd081cd95d606a1b60448201526064015b60405180910390fd5b61032181610502565b505b60015460405160009182916001600160a01b03909116903490610347908890610822565b60006040518083038185875af1925050503d8060008114610384576040519150601f19603f3d011682016040523d82523d6000602084013e610389565b606091505b5091509150816103ce5760405162461bcd60e51b815260206004820152601060248201526f195e1958dd5d1a5bdb8819985a5b195960821b604482015260640161030f565b7f57a62eca76fc623c92f161d2a4b851851ece707135ce2af1eec256d660571b6d8386836040516104019392919061083e565b60405180910390a1949350505050565b61041e600160fd1b610502565b6001600160e01b03198216600081815260026020908152604091829020849055815192835282018390527fdb8ed917742b49e83acd1322bcaa8f18b1e5f78a70784c43ea14db7ab50e628d910160405180910390a15050565b610488828051906020012082610411565b5050565b610499600160ff1b610502565b61048882826104f0336104c1876001600160a01b031660009081526020819052604090205490565b6001600160a01b0391909116600090815260208190526040902054600019808818821618908716919091171690565b610513565b6104ff308261048c565b50565b6104ff61050e8261056b565b61057d565b6001600160a01b0383166000818152602081815260409182902084905581518581529081018490527fe9be537308880e0f56b7d7cfd7abf85f14c4934486d138f848b92a0cbaf659b4910160405180910390a2505050565b6000610577338361059b565b92915050565b806104ff57604051634ca8886760e01b815260040160405180910390fd5b6001600160a01b038216600090815260208190526040812054821682145b9392505050565b634e487b7160e01b600052604160045260246000fd5b600067ffffffffffffffff808411156105f1576105f16105c0565b604051601f8501601f19908116603f01168101908282118183101715610619576106196105c0565b8160405280935085815286868601111561063257600080fd5b858560208301376000602087830101525050509392505050565b60006020828403121561065e57600080fd5b813567ffffffffffffffff81111561067557600080fd5b8201601f8101841361068657600080fd5b610695848235602084016105d6565b949350505050565b60005b838110156106b85781810151838201526020016106a0565b50506000910152565b600081518084526106d981602086016020860161069d565b601f01601f19169290920160200192915050565b6020815260006105b960208301846106c1565b80356001600160e01b03198116811461071857600080fd5b919050565b60006020828403121561072f57600080fd5b6105b982610700565b80356001600160a01b038116811461071857600080fd5b60006020828403121561076157600080fd5b6105b982610738565b6000806040838503121561077d57600080fd5b61078683610700565b946020939093013593505050565b600080604083850312156107a757600080fd5b823567ffffffffffffffff8111156107be57600080fd5b8301601f810185136107cf57600080fd5b6107de858235602084016105d6565b95602094909401359450505050565b6000806040838503121561080057600080fd5b61078683610738565b60006020828403121561081b57600080fd5b5035919050565b6000825161083481846020870161069d565b9190910192915050565b63ffffffff60e01b8416815260606020820152600061086060608301856106c1565b828103604084015261087281856106c1565b969550505050505056fea2646970667358221220e3c74e6e9d12e9f897ad014f9a9508e19f07aa6158ea118e7b0f262e5ef3629764736f6c63430008150033",
282
- "deployedBytecode": "0x6080604052600436106100a05760003560e01c8063491d261111610064578063491d2611146101d35780635defb40d146101f3578063ae5b102e14610213578063ae682e2e14610233578063d4b839921461024b578063d5bb7f6714610283576100bf565b806309c5eabe146100ff5780630e82fe25146101285780632b5214161461016357806334e48c9e14610185578063442767331461019d576100bf565b366100bf576100bd604051806020016040528060008152506102a3565b005b6100bd6000368080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506102a392505050565b61011261010d36600461064c565b6102a3565b60405161011f91906106ed565b60405180910390f35b34801561013457600080fd5b5061015561014336600461071d565b60026020526000908152604090205481565b60405190815260200161011f565b34801561016f57600080fd5b5030600090815260208190526040902054610155565b34801561019157600080fd5b50610155600160fd1b81565b3480156101a957600080fd5b506101556101b836600461074f565b6001600160a01b031660009081526020819052604090205490565b3480156101df57600080fd5b506100bd6101ee36600461076a565b610411565b3480156101ff57600080fd5b506100bd61020e366004610794565b610477565b34801561021f57600080fd5b506100bd61022e3660046107ed565b61048c565b34801561023f57600080fd5b50610155600160ff1b81565b34801561025757600080fd5b5060015461026b906001600160a01b031681565b6040516001600160a01b03909116815260200161011f565b34801561028f57600080fd5b506100bd61029e366004610809565b6104f5565b602081015181516060919015610323576001600160e01b03198116600090815260026020526040812054908190036103185760405162461bcd60e51b81526020600482015260136024820152721858d8d95cdcc81c9bdb19481b9bdd081cd95d606a1b60448201526064015b60405180910390fd5b61032181610502565b505b60015460405160009182916001600160a01b03909116903490610347908890610822565b60006040518083038185875af1925050503d8060008114610384576040519150601f19603f3d011682016040523d82523d6000602084013e610389565b606091505b5091509150816103ce5760405162461bcd60e51b815260206004820152601060248201526f195e1958dd5d1a5bdb8819985a5b195960821b604482015260640161030f565b7f57a62eca76fc623c92f161d2a4b851851ece707135ce2af1eec256d660571b6d8386836040516104019392919061083e565b60405180910390a1949350505050565b61041e600160fd1b610502565b6001600160e01b03198216600081815260026020908152604091829020849055815192835282018390527fdb8ed917742b49e83acd1322bcaa8f18b1e5f78a70784c43ea14db7ab50e628d910160405180910390a15050565b610488828051906020012082610411565b5050565b610499600160ff1b610502565b61048882826104f0336104c1876001600160a01b031660009081526020819052604090205490565b6001600160a01b0391909116600090815260208190526040902054600019808818821618908716919091171690565b610513565b6104ff308261048c565b50565b6104ff61050e8261056b565b61057d565b6001600160a01b0383166000818152602081815260409182902084905581518581529081018490527fe9be537308880e0f56b7d7cfd7abf85f14c4934486d138f848b92a0cbaf659b4910160405180910390a2505050565b6000610577338361059b565b92915050565b806104ff57604051634ca8886760e01b815260040160405180910390fd5b6001600160a01b038216600090815260208190526040812054821682145b9392505050565b634e487b7160e01b600052604160045260246000fd5b600067ffffffffffffffff808411156105f1576105f16105c0565b604051601f8501601f19908116603f01168101908282118183101715610619576106196105c0565b8160405280935085815286868601111561063257600080fd5b858560208301376000602087830101525050509392505050565b60006020828403121561065e57600080fd5b813567ffffffffffffffff81111561067557600080fd5b8201601f8101841361068657600080fd5b610695848235602084016105d6565b949350505050565b60005b838110156106b85781810151838201526020016106a0565b50506000910152565b600081518084526106d981602086016020860161069d565b601f01601f19169290920160200192915050565b6020815260006105b960208301846106c1565b80356001600160e01b03198116811461071857600080fd5b919050565b60006020828403121561072f57600080fd5b6105b982610700565b80356001600160a01b038116811461071857600080fd5b60006020828403121561076157600080fd5b6105b982610738565b6000806040838503121561077d57600080fd5b61078683610700565b946020939093013593505050565b600080604083850312156107a757600080fd5b823567ffffffffffffffff8111156107be57600080fd5b8301601f810185136107cf57600080fd5b6107de858235602084016105d6565b95602094909401359450505050565b6000806040838503121561080057600080fd5b61078683610738565b60006020828403121561081b57600080fd5b5035919050565b6000825161083481846020870161069d565b9190910192915050565b63ffffffff60e01b8416815260606020820152600061086060608301856106c1565b828103604084015261087281856106c1565b969550505050505056fea2646970667358221220e3c74e6e9d12e9f897ad014f9a9508e19f07aa6158ea118e7b0f262e5ef3629764736f6c63430008150033",
281
+ "bytecode": "0x608060405234801561001057600080fd5b50604051610a32380380610a3283398101604081905261002f9161013e565b8060006001600160a01b0382161561004e5761004e82600019806100ca565b6100593082806100ca565b50506001600160a01b0382166100a45760405162461bcd60e51b815260206004820152600c60248201526b7a65726f206164647265737360a01b604482015260640160405180910390fd5b50600180546001600160a01b0319166001600160a01b0392909216919091179055610171565b6001600160a01b0383166000818152602081815260409182902084905581518581529081018490527fe9be537308880e0f56b7d7cfd7abf85f14c4934486d138f848b92a0cbaf659b4910160405180910390a2505050565b80516001600160a01b038116811461013957600080fd5b919050565b6000806040838503121561015157600080fd5b61015a83610122565b915061016860208401610122565b90509250929050565b6108b2806101806000396000f3fe6080604052600436106100a05760003560e01c8063491d261111610064578063491d2611146101d35780635defb40d146101f3578063ae5b102e14610213578063ae682e2e14610233578063d4b839921461024b578063d5bb7f6714610283576100bf565b806309c5eabe146100ff5780630e82fe25146101285780632b5214161461016357806334e48c9e14610185578063442767331461019d576100bf565b366100bf576100bd604051806020016040528060008152506102a3565b005b6100bd6000368080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506102a392505050565b61011261010d36600461064c565b6102a3565b60405161011f91906106ed565b60405180910390f35b34801561013457600080fd5b5061015561014336600461071d565b60026020526000908152604090205481565b60405190815260200161011f565b34801561016f57600080fd5b5030600090815260208190526040902054610155565b34801561019157600080fd5b50610155600160fd1b81565b3480156101a957600080fd5b506101556101b836600461074f565b6001600160a01b031660009081526020819052604090205490565b3480156101df57600080fd5b506100bd6101ee36600461076a565b610411565b3480156101ff57600080fd5b506100bd61020e366004610794565b610477565b34801561021f57600080fd5b506100bd61022e3660046107ed565b61048c565b34801561023f57600080fd5b50610155600160ff1b81565b34801561025757600080fd5b5060015461026b906001600160a01b031681565b6040516001600160a01b03909116815260200161011f565b34801561028f57600080fd5b506100bd61029e366004610809565b6104f5565b602081015181516060919015610323576001600160e01b03198116600090815260026020526040812054908190036103185760405162461bcd60e51b81526020600482015260136024820152721858d8d95cdcc81c9bdb19481b9bdd081cd95d606a1b60448201526064015b60405180910390fd5b61032181610502565b505b60015460405160009182916001600160a01b03909116903490610347908890610822565b60006040518083038185875af1925050503d8060008114610384576040519150601f19603f3d011682016040523d82523d6000602084013e610389565b606091505b5091509150816103ce5760405162461bcd60e51b815260206004820152601060248201526f195e1958dd5d1a5bdb8819985a5b195960821b604482015260640161030f565b7f57a62eca76fc623c92f161d2a4b851851ece707135ce2af1eec256d660571b6d8386836040516104019392919061083e565b60405180910390a1949350505050565b61041e600160fd1b610502565b6001600160e01b03198216600081815260026020908152604091829020849055815192835282018390527fdb8ed917742b49e83acd1322bcaa8f18b1e5f78a70784c43ea14db7ab50e628d910160405180910390a15050565b610488828051906020012082610411565b5050565b610499600160ff1b610502565b61048882826104f0336104c1876001600160a01b031660009081526020819052604090205490565b6001600160a01b0391909116600090815260208190526040902054600019808818821618908716919091171690565b610513565b6104ff308261048c565b50565b6104ff61050e8261056b565b61057d565b6001600160a01b0383166000818152602081815260409182902084905581518581529081018490527fe9be537308880e0f56b7d7cfd7abf85f14c4934486d138f848b92a0cbaf659b4910160405180910390a2505050565b6000610577338361059b565b92915050565b806104ff57604051634ca8886760e01b815260040160405180910390fd5b6001600160a01b038216600090815260208190526040812054821682145b9392505050565b634e487b7160e01b600052604160045260246000fd5b600067ffffffffffffffff808411156105f1576105f16105c0565b604051601f8501601f19908116603f01168101908282118183101715610619576106196105c0565b8160405280935085815286868601111561063257600080fd5b858560208301376000602087830101525050509392505050565b60006020828403121561065e57600080fd5b813567ffffffffffffffff81111561067557600080fd5b8201601f8101841361068657600080fd5b610695848235602084016105d6565b949350505050565b60005b838110156106b85781810151838201526020016106a0565b50506000910152565b600081518084526106d981602086016020860161069d565b601f01601f19169290920160200192915050565b6020815260006105b960208301846106c1565b80356001600160e01b03198116811461071857600080fd5b919050565b60006020828403121561072f57600080fd5b6105b982610700565b80356001600160a01b038116811461071857600080fd5b60006020828403121561076157600080fd5b6105b982610738565b6000806040838503121561077d57600080fd5b61078683610700565b946020939093013593505050565b600080604083850312156107a757600080fd5b823567ffffffffffffffff8111156107be57600080fd5b8301601f810185136107cf57600080fd5b6107de858235602084016105d6565b95602094909401359450505050565b6000806040838503121561080057600080fd5b61078683610738565b60006020828403121561081b57600080fd5b5035919050565b6000825161083481846020870161069d565b9190910192915050565b63ffffffff60e01b8416815260606020820152600061086060608301856106c1565b828103604084015261087281856106c1565b969550505050505056fea26469706673582212209161ccf77ca651079cf7ca63ab02af76c981b46d82708b27350727dd4292764464736f6c63430008150033",
282
+ "deployedBytecode": "0x6080604052600436106100a05760003560e01c8063491d261111610064578063491d2611146101d35780635defb40d146101f3578063ae5b102e14610213578063ae682e2e14610233578063d4b839921461024b578063d5bb7f6714610283576100bf565b806309c5eabe146100ff5780630e82fe25146101285780632b5214161461016357806334e48c9e14610185578063442767331461019d576100bf565b366100bf576100bd604051806020016040528060008152506102a3565b005b6100bd6000368080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506102a392505050565b61011261010d36600461064c565b6102a3565b60405161011f91906106ed565b60405180910390f35b34801561013457600080fd5b5061015561014336600461071d565b60026020526000908152604090205481565b60405190815260200161011f565b34801561016f57600080fd5b5030600090815260208190526040902054610155565b34801561019157600080fd5b50610155600160fd1b81565b3480156101a957600080fd5b506101556101b836600461074f565b6001600160a01b031660009081526020819052604090205490565b3480156101df57600080fd5b506100bd6101ee36600461076a565b610411565b3480156101ff57600080fd5b506100bd61020e366004610794565b610477565b34801561021f57600080fd5b506100bd61022e3660046107ed565b61048c565b34801561023f57600080fd5b50610155600160ff1b81565b34801561025757600080fd5b5060015461026b906001600160a01b031681565b6040516001600160a01b03909116815260200161011f565b34801561028f57600080fd5b506100bd61029e366004610809565b6104f5565b602081015181516060919015610323576001600160e01b03198116600090815260026020526040812054908190036103185760405162461bcd60e51b81526020600482015260136024820152721858d8d95cdcc81c9bdb19481b9bdd081cd95d606a1b60448201526064015b60405180910390fd5b61032181610502565b505b60015460405160009182916001600160a01b03909116903490610347908890610822565b60006040518083038185875af1925050503d8060008114610384576040519150601f19603f3d011682016040523d82523d6000602084013e610389565b606091505b5091509150816103ce5760405162461bcd60e51b815260206004820152601060248201526f195e1958dd5d1a5bdb8819985a5b195960821b604482015260640161030f565b7f57a62eca76fc623c92f161d2a4b851851ece707135ce2af1eec256d660571b6d8386836040516104019392919061083e565b60405180910390a1949350505050565b61041e600160fd1b610502565b6001600160e01b03198216600081815260026020908152604091829020849055815192835282018390527fdb8ed917742b49e83acd1322bcaa8f18b1e5f78a70784c43ea14db7ab50e628d910160405180910390a15050565b610488828051906020012082610411565b5050565b610499600160ff1b610502565b61048882826104f0336104c1876001600160a01b031660009081526020819052604090205490565b6001600160a01b0391909116600090815260208190526040902054600019808818821618908716919091171690565b610513565b6104ff308261048c565b50565b6104ff61050e8261056b565b61057d565b6001600160a01b0383166000818152602081815260409182902084905581518581529081018490527fe9be537308880e0f56b7d7cfd7abf85f14c4934486d138f848b92a0cbaf659b4910160405180910390a2505050565b6000610577338361059b565b92915050565b806104ff57604051634ca8886760e01b815260040160405180910390fd5b6001600160a01b038216600090815260208190526040812054821682145b9392505050565b634e487b7160e01b600052604160045260246000fd5b600067ffffffffffffffff808411156105f1576105f16105c0565b604051601f8501601f19908116603f01168101908282118183101715610619576106196105c0565b8160405280935085815286868601111561063257600080fd5b858560208301376000602087830101525050509392505050565b60006020828403121561065e57600080fd5b813567ffffffffffffffff81111561067557600080fd5b8201601f8101841361068657600080fd5b610695848235602084016105d6565b949350505050565b60005b838110156106b85781810151838201526020016106a0565b50506000910152565b600081518084526106d981602086016020860161069d565b601f01601f19169290920160200192915050565b6020815260006105b960208301846106c1565b80356001600160e01b03198116811461071857600080fd5b919050565b60006020828403121561072f57600080fd5b6105b982610700565b80356001600160a01b038116811461071857600080fd5b60006020828403121561076157600080fd5b6105b982610738565b6000806040838503121561077d57600080fd5b61078683610700565b946020939093013593505050565b600080604083850312156107a757600080fd5b823567ffffffffffffffff8111156107be57600080fd5b8301601f810185136107cf57600080fd5b6107de858235602084016105d6565b95602094909401359450505050565b6000806040838503121561080057600080fd5b61078683610738565b60006020828403121561081b57600080fd5b5035919050565b6000825161083481846020870161069d565b9190910192915050565b63ffffffff60e01b8416815260606020820152600061086060608301856106c1565b828103604084015261087281856106c1565b969550505050505056fea26469706673582212209161ccf77ca651079cf7ca63ab02af76c981b46d82708b27350727dd4292764464736f6c63430008150033",
283
283
  "linkReferences": {},
284
284
  "deployedLinkReferences": {}
285
285
  }
@@ -5,7 +5,7 @@
5
5
  // hardhat (0.4.11)
6
6
  pragma solidity >=0.8.4;
7
7
 
8
- import {AccessControlCore} from "./AccessControlCore.sol";
8
+ import "./AccessControlCore.sol";
9
9
 
10
10
  /**
11
11
  * @title Role-based Access Control (RBAC)
@@ -56,7 +56,7 @@ import {AccessControlCore} from "./AccessControlCore.sol";
56
56
  *
57
57
  * @author Basil Gorin
58
58
  */
59
- contract AccessControl is AccessControlCore {
59
+ abstract contract AccessControl is AccessControlCore {
60
60
  /**
61
61
  * @notice Creates an access control instance, setting the contract owner to have full privileges
62
62
  *
@@ -135,8 +135,11 @@ abstract contract AccessControlCore {
135
135
  * @param _features initial features mask of the contract, can be zero
136
136
  */
137
137
  constructor(address _owner, uint256 _features) { // visibility modifier is required to be compilable with 0.6.x
138
- // grant owner full privileges
139
- __setRole(_owner, FULL_PRIVILEGES_MASK, FULL_PRIVILEGES_MASK);
138
+ // if there is a request to set owner (zero address owner means no owner)
139
+ if(_owner != address(0)) {
140
+ // grant owner full privileges
141
+ __setRole(_owner, FULL_PRIVILEGES_MASK, FULL_PRIVILEGES_MASK);
142
+ }
140
143
  // update initial features bitmask
141
144
  __setRole(address(this), _features, _features);
142
145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazy-sol/access-control",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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": {
@@ -21,14 +21,12 @@
21
21
  ],
22
22
  "author": "Basil Gorin",
23
23
  "license": "MIT",
24
- "dependencies": {
25
- "@lazy-sol/a-missing-gem": "^1.0.9",
26
- "@nomiclabs/hardhat-truffle5": "^2.0.7",
27
- "hardhat": "^2.22.9",
28
- "hardhat-deploy": "^0.11.45"
29
- },
30
24
  "devDependencies": {
25
+ "@lazy-sol/a-missing-gem": "^1.0.11",
31
26
  "@lazy-sol/zeppelin-test-helpers": "^1.0.5",
27
+ "@nomiclabs/hardhat-truffle5": "^2.0.7",
28
+ "hardhat": "^2.22.13",
29
+ "hardhat-deploy": "^0.11.45",
32
30
  "hardhat-gas-reporter": "^1.0.10",
33
31
  "solidity-coverage": "^0.8.13"
34
32
  },
@@ -3,23 +3,14 @@
3
3
  * (doesn't return any value on successful operation)
4
4
  *
5
5
  * @param a0 smart contract owner
6
- * @param H0 initial token holder address
7
6
  * @returns USDT ERC20 instance
8
7
  */
9
- async function deploy_usdt(a0, H0 = a0) {
8
+ async function deploy_usdt(a0) {
10
9
  // smart contracts required
11
10
  const USDTContract = artifacts.require("TetherToken");
12
11
 
13
- // deploy the token
14
- const token = await USDTContract.new(0, "Tether USD", "USDT", 6, {from: a0});
15
-
16
- // move the initial supply if required
17
- if(H0 !== a0) {
18
- await token.transfer(H0, S0, {from: a0});
19
- }
20
-
21
- // return the reference
22
- return token;
12
+ // deploy the token and return the reference
13
+ return await USDTContract.new(0, "Tether USD", "USDT", 6, {from: a0});
23
14
  }
24
15
 
25
16
  /**
@@ -59,13 +59,15 @@ 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
+ operator: owner,
66
+ requested: FULL_PRIVILEGES_MASK,
67
+ assigned: FULL_PRIVILEGES_MASK,
68
+ });
67
69
  });
68
- });
70
+ }
69
71
  it('"RoleUpdated(this)" event is emitted correctly', async function() {
70
72
  await expectEvent.inConstruction(access_control, "RoleUpdated", {
71
73
  operator: access_control.address,
@@ -73,9 +75,16 @@ function behavesLikeRBAC(deployment_fn, a0, a1, a2) {
73
75
  assigned: features,
74
76
  });
75
77
  });
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
- });
78
+ if(owner !== ZERO_ADDRESS) {
79
+ it("owners' role is set correctly", async function() {
80
+ expect(await access_control.getRole(owner)).to.be.bignumber.that.equals(FULL_PRIVILEGES_MASK);
81
+ });
82
+ }
83
+ else {
84
+ it("owners' role is not set", async function() {
85
+ expect(await access_control.getRole(owner)).to.be.bignumber.that.equals("0");
86
+ });
87
+ }
79
88
  it("features are set correctly", async function() {
80
89
  expect(await access_control.features()).to.be.bignumber.that.equals(features);
81
90
  });
package/ui.html ADDED
@@ -0,0 +1,506 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Role-based Access Control (RBAC) Inspector</title>
6
+ <!-- Include Web3.js from a CDN -->
7
+ <script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
8
+ <style>
9
+ table#roles_list {
10
+ width: 100%;
11
+ border-collapse: collapse;
12
+ }
13
+ table#roles_list th, table#roles_list td {
14
+ border: 1px solid grey;
15
+ }
16
+ table#roles_list tr td {
17
+ font-family: monospace;
18
+ }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <fieldset id="root_container" style="display: none;"><legend id="connection_state" style="text-align: right;"></legend>
23
+ <div id="main_app" style="display: none;">
24
+ <fieldset><legend>Role-based Access Control (RBAC) Features and Roles Inspector</legend>
25
+ <form onsubmit="inspect_features_roles(event)">
26
+ <table>
27
+ <tr>
28
+ <td><label for="contract_address">Contract Address:</label></td>
29
+ <td><input id="contract_address" type="text" required size="42" style="font-family: monospace;" value="0xA9f50518f9119d5F7A324EC76481c5246970dEa6"/></td>
30
+ </tr>
31
+ <tr><td></td><td><input type="submit" value="Inspect"/></td></tr>
32
+ </table>
33
+ </form>
34
+ </fieldset>
35
+ <div id="features_roles_container" style="display: none;">
36
+ <fieldset>
37
+ <legend id="features_title">RBAC Features</legend>
38
+ <div id="features_hex">Loading...</div>
39
+ </fieldset>
40
+ <fieldset>
41
+ <legend id="roles_list_title">RBAC Operators and Roles</legend>
42
+ <table id="roles_list"><tr><td>Loading...</td></tr></table>
43
+ </fieldset>
44
+ </div>
45
+ </div>
46
+ </fieldset>
47
+ <div style="position: fixed; left: 0; bottom: 0; margin: 0.1em 0.2em;">&copy; 2024 <a href="https://github.com/lazy-sol/">Lazy So[u]l</a></div>
48
+ <div style="position: fixed; right: 0; bottom: 0; margin: 0.1em 0.2em; font-family: monospace;">
49
+ [<a href="https://lazy-sol.github.io/advanced-erc20/ui.html">Advanced ERC20</a>]
50
+ [<a href="https://lazy-sol.github.io/tiny-erc721/ui.html">Tiny ERC721</a>]
51
+ [RBAC Inspector]
52
+ </div>
53
+ </body>
54
+ <script type="text/javascript">
55
+ // verify if MetaMask is installed
56
+ if(!window.ethereum) {
57
+ if(window.confirm("MetaMask is not installed. You will be redirected to the MetaMask installation page.")) {
58
+ window.setTimeout(function() {
59
+ window.location.href = "https://metamask.io/download/";
60
+ }, 0);
61
+ }
62
+ window.stop();
63
+ }
64
+ </script>
65
+
66
+ <script type="text/javascript">
67
+ // static HTML page anchors
68
+ const root_container = document.getElementById("root_container");
69
+ const connection_state = document.getElementById("connection_state");
70
+ const main_app = document.getElementById("main_app");
71
+ const contract_address = document.getElementById("contract_address");
72
+ const features_roles_container = document.getElementById("features_roles_container");
73
+ const features_title = document.getElementById("features_title");
74
+ const features_hex = document.getElementById("features_hex");
75
+ const roles_list_title = document.getElementById("roles_list_title");
76
+ const roles_list = document.getElementById("roles_list");
77
+ </script>
78
+
79
+ <script type="text/javascript">
80
+ // auxiliary pure functions
81
+
82
+ // auxiliary function to log and display non-fatal error, doesn't stop execution
83
+ function non_fatal_error(message, error) {
84
+ console.error("%s: %o", message, error);
85
+ window.alert(message + (error? ": " + json_stringify(error): ""));
86
+ }
87
+
88
+ // auxiliary function to log and display fatal error and stop execution
89
+ function fatal_error(message, error) {
90
+ non_fatal_error("FATAL: " + message, error);
91
+ window.stop();
92
+ throw error? error: "error";
93
+ }
94
+
95
+ // save JSON.stringify works properly with BigInt
96
+ function json_stringify(input) {
97
+ return JSON.stringify(input, (key, value) =>
98
+ typeof value === 'bigint' ? value.toString() : value
99
+ );
100
+ }
101
+ </script>
102
+
103
+ <script type="text/javascript">
104
+ // config and app state
105
+
106
+ // config stores settings which don't change
107
+ const CONF = {
108
+ // supported networks, where our contracts are known to be deployed
109
+ chains: {
110
+ 1: {
111
+ name: "Mainnet",
112
+ block_explorer: {
113
+ tx: "https://etherscan.io/tx/",
114
+ address: "https://etherscan.io/address/",
115
+ token: "https://etherscan.io/token/",
116
+ },
117
+ },
118
+ 11155111: {
119
+ name: "Sepolia",
120
+ block_explorer: {
121
+ tx: "https://sepolia.etherscan.io/tx/",
122
+ address: "https://sepolia.etherscan.io/address/",
123
+ token: "https://sepolia.etherscan.io/token/",
124
+ },
125
+ },
126
+ 8453: {
127
+ name: "Base",
128
+ block_explorer: {
129
+ tx: "https://basescan.org/tx/",
130
+ address: "https://basescan.org/address/",
131
+ token: "https://basescan.org/token/",
132
+ },
133
+ },
134
+ 84532: {
135
+ name: "Base Sepolia",
136
+ block_explorer: {
137
+ tx: "https://sepolia.basescan.org//tx/",
138
+ address: "https://sepolia.basescan.org/address/",
139
+ token: "https://sepolia.basescan.org/token/",
140
+ },
141
+ },
142
+ },
143
+ };
144
+
145
+ // state stores settings which may change due to user actions,
146
+ // due to the async nature of the app, many incoming events, it makes sense to follow
147
+ // the state of the app by listening to all the events and updating the state
148
+ const STATE = {
149
+ // currently connected network (recognized decimal Chain ID)
150
+ chain_id: undefined,
151
+ // currently connected account (accounts[0])
152
+ A0: undefined,
153
+ // currently connected chain config
154
+ chain: function() {
155
+ return this.chain_id? CONF.chains[this.chain_id]: undefined;
156
+ },
157
+ // function to display the chain we're connected to
158
+ update_chain_id: function(chain_id) {
159
+ this.chain_id = chain_id? parseInt(chain_id): undefined;
160
+ },
161
+ // function to update currently connected account
162
+ update_A0: function(accounts) {
163
+ // MetaMask is locked or not connected
164
+ if(!accounts || !accounts.length) {
165
+ this.A0 = undefined;
166
+ }
167
+ else {
168
+ [this.A0] = accounts;
169
+ }
170
+ },
171
+ // function to refresh the connection state UI
172
+ refresh_ui: function() {
173
+ if(CONF.chains[this.chain_id]) {
174
+ if(this.A0) {
175
+ connection_state.innerHTML = `Connected <span style="font-family: monospace;">${this.A0}</span>`;
176
+ }
177
+ else {
178
+ connection_state.innerHTML = `
179
+ MetaMask is not connected.
180
+ <input type="button" onclick="metamask_connect()" value="Connect"/>
181
+ `;
182
+ }
183
+ }
184
+ else if(this.chain_id) {
185
+ connection_state.innerHTML = `
186
+ Unsupported Chain ID: ${this.chain_id}
187
+ <input type="button" onclick="add_sepolia_network()" value="Switch to Sepolia"/>
188
+ `;
189
+ }
190
+ else {
191
+ connection_state.innerHTML = `
192
+ MetaMask disconnected.
193
+ <input type="button" onclick="metamask_connect()" value="Connect"/>
194
+ `;
195
+ }
196
+ root_container.style["display"] = "block";
197
+ },
198
+ // function to check if we're connected to supported network
199
+ connected: function() {
200
+ return this.chain() && this.A0;
201
+ },
202
+ }
203
+ </script>
204
+
205
+ <script type="text/javascript">
206
+ // main app section
207
+
208
+ // define window.ethereum shortcut
209
+ const ethereum = window.ethereum;
210
+ // MetaMask is installed, create a new Web3 instance
211
+ const web3 = new Web3(window.ethereum);
212
+
213
+ // function to connect to MetaMask
214
+ function metamask_connect() {
215
+ ethereum.request({ method: 'eth_requestAccounts' }).then(function(accounts) {
216
+ STATE.update_A0(accounts);
217
+ STATE.refresh_ui();
218
+ reload_main_app();
219
+ }).catch(function(e) {
220
+ non_fatal_error("Error connecting MetaMask", e);
221
+ });
222
+ }
223
+
224
+ // check if MetaMask is connected
225
+ ethereum.request({ method: 'eth_accounts' }).then(function(accounts) {
226
+ // check current connected network
227
+ ethereum.request({ method: 'eth_chainId' }).then(function(chain_id) {
228
+ STATE.update_chain_id(chain_id);
229
+ STATE.update_A0(accounts);
230
+ STATE.refresh_ui();
231
+ reload_main_app();
232
+ }).catch(function(e) {
233
+ fatal_error("Error getting Chain ID", e);
234
+ });
235
+ }).catch(function(e) {
236
+ fatal_error("Error connecting to MetaMask", e);
237
+ });
238
+
239
+ // Handle the case when MetaMask connects another account
240
+ ethereum.on('accountsChanged', function(accounts) {
241
+ console.log("account has been switched", accounts)
242
+ STATE.update_A0(accounts);
243
+ STATE.refresh_ui();
244
+ reload_main_app();
245
+ });
246
+
247
+ // Handle the case when MetaMask is connected
248
+ ethereum.on('connect', function(connect_info) {
249
+ console.log('MetaMask has been connected', connect_info);
250
+ STATE.update_chain_id(connect_info.chainId);
251
+ STATE.refresh_ui();
252
+ reload_main_app();
253
+ });
254
+
255
+ // MetaMask disconnect listener
256
+ ethereum.on('disconnect', function(error) {
257
+ console.warn('MetaMask has been disconnected', error);
258
+ STATE.update_A0();
259
+ STATE.update_chain_id();
260
+ STATE.refresh_ui();
261
+ reload_main_app();
262
+ });
263
+
264
+ // network switch listener
265
+ ethereum.on('chainChanged', function(chain_id) {
266
+ console.log('network has been changed', chain_id)
267
+ STATE.update_chain_id(chain_id);
268
+ STATE.refresh_ui();
269
+ reload_main_app();
270
+ });
271
+
272
+ // define an async routine to switch the network to Sepolia
273
+ async function add_sepolia_network() {
274
+ const sepolia_chain_id = '0xaa36a7'; // Hexadecimal chain ID for Sepolia
275
+
276
+ try {
277
+ // Check if Sepolia is already added
278
+ await ethereum.request({
279
+ method: 'wallet_switchEthereumChain',
280
+ params: [{ chainId: sepolia_chain_id }],
281
+ });
282
+ }
283
+ catch(e) {
284
+ // Error code 4902 indicates that the chain has not been added to MetaMask
285
+ if(e.code !== 4902) {
286
+ // Handle errors when switching the network
287
+ fatal_error("Error switching network to Sepolia", e)
288
+ }
289
+
290
+ try {
291
+ // Add the Sepolia network
292
+ await ethereum.request({
293
+ method: 'wallet_addEthereumChain',
294
+ params: [{
295
+ chainId: sepolia_chain_id,
296
+ rpcUrl: 'https://sepolia.infura.io/v3/',
297
+ chainName: 'Sepolia Test Network',
298
+ nativeCurrency: {
299
+ name: 'SepoliaETH',
300
+ symbol: 'SepoliaETH',
301
+ decimals: 18,
302
+ },
303
+ blockExplorerUrls: ['https://sepolia.etherscan.io/'],
304
+ }],
305
+ });
306
+ }
307
+ catch (e) {
308
+ // Handle errors when adding the network
309
+ fatal_error("Error adding Sepolia network", e);
310
+ }
311
+ }
312
+ }
313
+
314
+ // writes the entire roles_list table
315
+ function features_roles_update({features, roles, assignments}) {
316
+ if(!Array.isArray(roles)) {
317
+ roles = Object.entries(roles).map(([operator, role]) => Object.assign({}, {operator, role}));
318
+ }
319
+
320
+ if(assignments && assignments.length) {
321
+ features_hex.innerHTML = features?
322
+ `Features enabled: <span style="font-family: monospace">0x${features.toString(16).toUpperCase()}</span>`:
323
+ "No features enabled.";
324
+
325
+ if(roles && roles.length) {
326
+ const table_body = roles.map(render_role_row).join("\n");
327
+ roles_list.innerHTML = `<tr><th>Operator Address</th><th>Assigned Role</th></tr>\n${table_body}`;
328
+ }
329
+ else {
330
+ roles_list.innerHTML = "No roles assigned.";
331
+ }
332
+ }
333
+ else {
334
+ features_hex.innerHTML = "No features enabled or not an RBAC contract.";
335
+ roles_list.innerHTML = "No roles assigned or not an RBAC contract.";
336
+ }
337
+ features_roles_container.style["display"] = "block";
338
+ }
339
+
340
+ // renders an HTML of the role list table row
341
+ function render_role_row({operator, role}) {
342
+ return `<tr><td>${operator}</td><td>0x${role.toString(16).toUpperCase()}</td></tr>`;
343
+ }
344
+
345
+ // reloads the entire app, used when network or accounts have been changed
346
+ function reload_main_app() {
347
+ if(!STATE.connected()) {
348
+ main_app.style["display"] = "none";
349
+ return;
350
+ }
351
+
352
+ features_roles_container.style.display = "none";
353
+ features_title.innerHTML = "RBAC Features";
354
+ features_hex.innerHTML = "Loading features...";
355
+ roles_list_title.innerHTML = "RBAC Operators and Roles";
356
+ roles_list.innerHTML = "Loading operators and roles...";
357
+
358
+ main_app.style["display"] = "block";
359
+ }
360
+
361
+ // function to extract RBAC roles from the target contract
362
+ async function extract_rbac_features_roles(target_address) {
363
+ // the earliest versions of the AccessControl emitted both FeaturesUpdated and RoleUpdated;
364
+ // the features were assigned to a zero address
365
+ // TODO: the earliest versions didn't emit an event on deployment
366
+ // later versions of the AccessControl emitted only RoleUpdated event;
367
+ // the features were assigned to a zero address, and later to the contract address (self-role)
368
+ // the most recent versions of the AccessControl emit only RoleUpdated event;
369
+ // the features are assigned to the contract address, argument naming is improved and made clearer
370
+ const v0_abi = [{
371
+ // the earliest versions only
372
+ "name": "FeaturesUpdated",
373
+ "type": "event",
374
+ "anonymous": false,
375
+ "inputs": [
376
+ {"indexed": true, "internalType": "address", "name": "_by", "type": "address"},
377
+ {"indexed": false, "internalType": "uint256", "name": "_requested", "type": "uint256"},
378
+ {"indexed": false, "internalType": "uint256", "name": "_actual", "type": "uint256"}
379
+ ],
380
+ }, {
381
+ // both the earliest and later versions
382
+ "name": "RoleUpdated",
383
+ "type": "event",
384
+ "anonymous": false,
385
+ "inputs": [
386
+ {"indexed": true, "internalType": "address", "name": "_by", "type": "address"},
387
+ {"indexed": true, "internalType": "address", "name": "_to", "type": "address"},
388
+ {"indexed": false, "internalType": "uint256", "name": "_requested", "type": "uint256"},
389
+ {"indexed": false, "internalType": "uint256", "name": "_actual", "type": "uint256"}
390
+ ],
391
+ }, {
392
+ // both the earliest and later versions
393
+ "name": "userRoles",
394
+ "type": "function",
395
+ "inputs": [{"internalType": "address", "name": "", "type": "address"}],
396
+ "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
397
+ "stateMutability": "view",
398
+ }];
399
+ const v1_abi = [{
400
+ // the latest versions only
401
+ "name": "RoleUpdated",
402
+ "type": "event",
403
+ "anonymous": false,
404
+ "inputs": [
405
+ {"indexed": true, "internalType": "address", "name": "operator", "type": "address"},
406
+ {"indexed": false, "internalType": "uint256", "name": "requested", "type": "uint256"},
407
+ {"indexed": false, "internalType": "uint256", "name": "assigned", "type": "uint256"}
408
+ ],
409
+ }, {
410
+ // the latest versions only
411
+ "name": "getRole",
412
+ "type": "function",
413
+ "inputs": [{"internalType": "address", "name": "operator", "type": "address"}],
414
+ "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
415
+ "stateMutability": "view",
416
+ }];
417
+
418
+ const v0_contract = new web3.eth.Contract(v0_abi, target_address);
419
+ const v1_contract = new web3.eth.Contract(v1_abi, target_address);
420
+
421
+ const features_v0 = await v0_contract.getPastEvents("FeaturesUpdated", {
422
+ fromBlock: "earliest",
423
+ });
424
+ console.log("v0.getPastEvents(FeaturesUpdated)", features_v0);
425
+ const role_v0 = await v0_contract.getPastEvents("RoleUpdated", {
426
+ fromBlock: "earliest",
427
+ });
428
+ console.log("v0.getPastEvents(RoleUpdated)", role_v0);
429
+ const role_v1 = await v1_contract.getPastEvents("RoleUpdated", {
430
+ fromBlock: "earliest",
431
+ });
432
+ console.log("v1.getPastEvents(RoleUpdated)", role_v1);
433
+ const assignments = features_v0.map(function(event) {
434
+ const {_actual: assigned} = event.returnValues;
435
+ // fix to the most recent format: feature is self-role
436
+ return {operator: target_address, assigned};
437
+ }).concat(role_v0.map(function(event) {
438
+ let {_to: operator, _actual: assigned} = event.returnValues;
439
+ if(operator === "0x0000000000000000000000000000000000000000") {
440
+ // fix to the most recent format: feature is self-role
441
+ operator = target_address;
442
+ }
443
+ return {operator, assigned};
444
+ })).concat(role_v1.map(function(event) {
445
+ const {operator, assigned} = event.returnValues;
446
+ return {operator, assigned};
447
+ }));
448
+
449
+ let features = 0;
450
+ v0_contract.methods["userRoles"]();
451
+
452
+ const roles = {};
453
+ assignments.forEach(function({operator, assigned}) {
454
+ if(operator === target_address) {
455
+ features = assigned;
456
+ }
457
+ else if(!BigInt(assigned)) {
458
+ delete roles[operator];
459
+ }
460
+ else {
461
+ roles[operator] = assigned;
462
+ }
463
+ });
464
+ console.log("extracted RBAC features and roles", features, roles);
465
+
466
+ return {features, roles, assignments};
467
+ }
468
+
469
+ // loads and parses features and roles on the RBAC contract,
470
+ // always returns false since is used as a form submit listener
471
+ function inspect_features_roles(e) {
472
+ if(e && e.preventDefault) {
473
+ e.preventDefault();
474
+ }
475
+
476
+ if(!contract_address.value) {
477
+ alert("Contract Address is required");
478
+ return false;
479
+ }
480
+ if(!STATE.connected()) {
481
+ alert("Not connected to the network. Please reload the page if the problem persists.")
482
+ return false;
483
+ }
484
+
485
+ web3.eth.getCode(contract_address.value).then(function(code) {
486
+ if(code.length < 4) {
487
+ alert("no code at " + contract_address.value);
488
+ }
489
+ }).catch(function(e) {
490
+ non_fatal_error("can't load contract at " + contract_address.value, e);
491
+ });
492
+
493
+ extract_rbac_features_roles(contract_address.value).then(features_roles_update).catch(function(e) {
494
+ console.warn("failed to load RBAC features and roles list", e);
495
+ features_hex.innerHTML = "Failed to load RBAC features: " + e;
496
+ roles_list.innerHTML = "Failed to load RBAC roles list: " + e;
497
+ features_roles_container.style["display"] = "block";
498
+ }).finally(function() {
499
+ features_title.innerHTML = "RBAC Features for " + contract_address.value;
500
+ roles_list_title.innerHTML = "RBAC Operators and Roles for " + contract_address.value;
501
+ });
502
+
503
+ return false;
504
+ }
505
+ </script>
506
+ </html>