@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 +7 -0
- package/README.md +69 -33
- package/artifacts/contracts/OwnableToAccessControlAdapter.sol/OwnableToAccessControlAdapter.json +2 -2
- package/contracts/AccessControl.sol +2 -2
- package/contracts/AccessControlCore.sol +5 -2
- package/package.json +5 -7
- package/test/include/deployment_routines.js +3 -12
- package/test/include/rbac.behaviour.js +18 -9
- package/ui.html +506 -0
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.
|
97
|
-
|
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/
|
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,
|
109
|
+
contract MyERC20Token is ERC20, AccessControlCore {
|
110
110
|
|
111
111
|
...
|
112
112
|
|
113
113
|
}
|
114
114
|
```
|
115
115
|
|
116
|
-
2.
|
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.
|
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
|
-
|
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
|
-
|
150
|
-
|
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.
|
181
|
-
|
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.
|
187
|
-
|
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.
|
190
|
-
|
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.
|
197
|
-
|
198
|
-
|
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.
|
205
|
-
|
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.
|
214
|
-
|
215
|
-
|
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.
|
222
|
-
|
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.
|
232
|
-
|
233
|
-
|
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,
|
package/artifacts/contracts/OwnableToAccessControlAdapter.sol/OwnableToAccessControlAdapter.json
CHANGED
@@ -278,8 +278,8 @@
|
|
278
278
|
"type": "receive"
|
279
279
|
}
|
280
280
|
],
|
281
|
-
"bytecode": "
|
282
|
-
"deployedBytecode": "
|
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
|
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
|
-
//
|
139
|
-
|
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.
|
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
|
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
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
77
|
-
|
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;">© 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>
|