@lazy-sol/access-control 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- package/.editorconfig +21 -0
- package/.solcover.js +39 -0
- package/CONTRIBUTING.md +200 -0
- package/CRIBBED_CODE.txt +26 -0
- package/LICENSE.txt +22 -0
- package/README.md +162 -0
- package/artifacts/contracts/OwnableToAccessControlAdapter.sol/OwnableToAccessControlAdapter.json +342 -0
- package/contracts/AccessControl.sol +305 -0
- package/contracts/AdapterFactory.sol +56 -0
- package/contracts/OwnableToAccessControlAdapter.sol +231 -0
- package/contracts/mocks/AccessControlMock.sol +14 -0
- package/contracts/mocks/TetherToken.sol +443 -0
- package/deploy/deploy-AdapterFactory.js +59 -0
- package/deployments/goerli/.chainId +1 -0
- package/deployments/goerli/AdapterFactory.json +104 -0
- package/deployments/goerli/solcInputs/9f8f20c7b4fd0796d45c56d37e790191.json +42 -0
- package/docs/commit_policy.md +201 -0
- package/docs/pull_request_template.md +40 -0
- package/docs/style_guides.md +240 -0
- package/hardhat.config.js +359 -0
- package/package.json +39 -0
- package/test/adapter_factory.js +54 -0
- package/test/include/deployment_routines.js +150 -0
- package/test/include/features_roles.js +42 -0
- package/test/include/rbac.behaviour.js +315 -0
- package/test/ownable_to_rbac_adapter.js +100 -0
- package/test/ownable_to_rbac_adapter_rbac.js +57 -0
- package/test/rbac_core.js +24 -0
- package/test/rbac_modifier.js +53 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
2
|
+
pragma solidity >=0.6.2;
|
3
|
+
|
4
|
+
import "./OwnableToAccessControlAdapter.sol";
|
5
|
+
|
6
|
+
/**
|
7
|
+
* @notice Ownable is a contract which is aware of its owner, that is has owner() function
|
8
|
+
*/
|
9
|
+
interface Ownable {
|
10
|
+
/**
|
11
|
+
* @notice Smart contract owner
|
12
|
+
*
|
13
|
+
* @return the address of the smart contract owner
|
14
|
+
*/
|
15
|
+
function owner() external returns(address);
|
16
|
+
}
|
17
|
+
|
18
|
+
/**
|
19
|
+
* @title Adapter Factory
|
20
|
+
*
|
21
|
+
* @notice Helper contract simplifying the deployment of the OwnableToAccessControlAdapter
|
22
|
+
*
|
23
|
+
* @author Basil Gorin
|
24
|
+
*/
|
25
|
+
contract AdapterFactory {
|
26
|
+
/**
|
27
|
+
* @dev Fired in deployNewOwnableToAccessControlAdapter
|
28
|
+
*
|
29
|
+
* @param adapterAddress newly deployed OwnableToAccessControlAdapter address
|
30
|
+
* @param ownableTargetAddress OZ Ownable target contract address
|
31
|
+
*/
|
32
|
+
event NewOwnableToAccessControlAdapterDeployed(address indexed adapterAddress, address indexed ownableTargetAddress);
|
33
|
+
|
34
|
+
/**
|
35
|
+
* @notice Deploys new OwnableToAccessControlAdapter bound to the OZ Ownable contract specified.
|
36
|
+
* Can be executed only by the OZ Ownable target owner. This owner is expected to transfer
|
37
|
+
* the ownership to the newly deployed OwnableToAccessControlAdapter contract address.
|
38
|
+
*
|
39
|
+
* @param targetAddress OZ Ownable target address to bind OwnableToAccessControlAdapter to
|
40
|
+
* @return address of the newly deployed OwnableToAccessControlAdapter contract
|
41
|
+
*/
|
42
|
+
function deployNewOwnableToAccessControlAdapter(address targetAddress) public returns(address) {
|
43
|
+
// verify sender is a target owner
|
44
|
+
require(Ownable(targetAddress).owner() == msg.sender, "not an owner");
|
45
|
+
|
46
|
+
// deploy the OwnableToAccessControlAdapter
|
47
|
+
// and set its ownership immediately to the tx executor (msg.sender)
|
48
|
+
address adapterAddress = address(new OwnableToAccessControlAdapter(targetAddress, msg.sender));
|
49
|
+
|
50
|
+
// emit an event
|
51
|
+
emit NewOwnableToAccessControlAdapterDeployed(adapterAddress, targetAddress);
|
52
|
+
|
53
|
+
// return address of the newly deployed OwnableToAccessControlAdapter contract
|
54
|
+
return adapterAddress;
|
55
|
+
}
|
56
|
+
}
|
@@ -0,0 +1,231 @@
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
2
|
+
pragma solidity >=0.6.2; // breaking changes in .call() (0.5.0), allow .call{}() (0.6.2)
|
3
|
+
|
4
|
+
import "./AccessControl.sol";
|
5
|
+
|
6
|
+
/**
|
7
|
+
* @title OZ Ownable to AccessControl Adapter (short: AccessControl Adapter)
|
8
|
+
*
|
9
|
+
* @notice Helper contract allowing to change the access model of the already deployed
|
10
|
+
* OpenZeppelin Ownable contract to the AccessControl model
|
11
|
+
*
|
12
|
+
* @dev Installation Flow
|
13
|
+
* Prerequisite: deployed OZ Ownable contract (target contract) address (target_address)
|
14
|
+
*
|
15
|
+
* 1. Deploy the AccessControl Adapter bound to the already deployed OZ Ownable contract
|
16
|
+
* (specify the target OZ Ownable contract address in the constructor upon the deployment)
|
17
|
+
*
|
18
|
+
* const adapter = await (artifacts.require("OwnableToAccessControlAdapter")).new(target_address);
|
19
|
+
*
|
20
|
+
* 2. Define what Ownable-restricted public functions on the target contract you'd like to be able
|
21
|
+
* to provide access to through the adapter contract
|
22
|
+
*
|
23
|
+
* 3. Map every such function with the role required to execute it using `updateAccessRole()` function
|
24
|
+
* For example, to be able to provide an access to the transferOwnership(address) function, you could do
|
25
|
+
*
|
26
|
+
* const ROLE_TRANSFER_OWNERSHIP_MANAGER = 0x00010000;
|
27
|
+
* await adapter.updateAccessRole("transferOwnership(address)", ROLE_TRANSFER_OWNERSHIP_MANAGER);
|
28
|
+
*
|
29
|
+
* 4. Provide the roles to the corresponding operators as you would usually do with AccessControl
|
30
|
+
* For example, if you wish an address 0x00000000000000000000000000000000000Ff1CE to grant an access to the
|
31
|
+
* transferOwnership(address) function on the target, you could do
|
32
|
+
*
|
33
|
+
* const operator = "0x00000000000000000000000000000000000Ff1CE";
|
34
|
+
* await adapter.updateRole(operator, ROLE_TRANSFER_OWNERSHIP_MANAGER);
|
35
|
+
*
|
36
|
+
* 5. Transfer the ownership of the target contract to the deployed AccessControl Adapter contract
|
37
|
+
* Note that you can also do steps 2-4 after the step 5
|
38
|
+
*
|
39
|
+
* @dev Usage Flow
|
40
|
+
* Prerequisite: installed AccessControl Adapter with the access to at least one restricted target contract
|
41
|
+
* function configured
|
42
|
+
*
|
43
|
+
* To execute the restricted access function on the target contract via the AccessControl Adapter
|
44
|
+
* 1. Use target contract ABI to construct a low-level function call calldata
|
45
|
+
* For example, to construct the transferOwnership() function calldata to transfer the ownership to the
|
46
|
+
* 0x00000000000000000000000000000000DEAdc0De address, you could do
|
47
|
+
*
|
48
|
+
* const to = "0x00000000000000000000000000000000DEAdc0De";
|
49
|
+
* const calldata = target.contract.methods.transferOwnership(to).encodeABI();
|
50
|
+
*
|
51
|
+
* 2. Execute a low-level function call on the AccessControl Adapter contract using the constructed calldata
|
52
|
+
* For example, to execute the transferOwnership() function (prepared in step 1), you could do
|
53
|
+
*
|
54
|
+
* await web3.eth.sendTransaction({
|
55
|
+
* from: operator,
|
56
|
+
* to: adapter.address,
|
57
|
+
* data: calldata,
|
58
|
+
* }
|
59
|
+
*
|
60
|
+
* 3. It is also ok to add an ether to the transaction by adding a value field to the `sendTransaction` call,
|
61
|
+
* as well as sending plain ether transfer transaction, as long as target contract has payable functions,
|
62
|
+
* and/or has a default payable receiver
|
63
|
+
*
|
64
|
+
* @author Basil Gorin
|
65
|
+
*/
|
66
|
+
contract OwnableToAccessControlAdapter is AccessControl {
|
67
|
+
/**
|
68
|
+
* @dev Target OZ Ownable contract AccessControl Adapter executes the transactions on
|
69
|
+
*
|
70
|
+
* @dev Target contract must transfer its ownership to the AccessControl Adapter
|
71
|
+
*/
|
72
|
+
address public target;
|
73
|
+
|
74
|
+
/**
|
75
|
+
* @dev Access roles mapping stores the roles required to access the functions on the
|
76
|
+
* target contract, guarding it from the unauthorized access
|
77
|
+
*
|
78
|
+
* @dev Maps function selector (bytes4) on the target contract to the access role (permission)
|
79
|
+
* required to execute the function
|
80
|
+
*/
|
81
|
+
mapping(bytes4 => uint256) public accessRoles;
|
82
|
+
|
83
|
+
/**
|
84
|
+
* @notice Access Roles manager is responsible for assigning the access roles to functions
|
85
|
+
*
|
86
|
+
* @dev Role ROLE_ACCESS_MANAGER allows modifying `accessRoles` mapping
|
87
|
+
*/
|
88
|
+
uint256 public constant ROLE_ACCESS_ROLES_MANAGER = 0x2000000000000000000000000000000000000000000000000000000000000000;
|
89
|
+
|
90
|
+
/**
|
91
|
+
* @dev Fired in `updateAccessRole` when the `accessRoles` mapping is updated
|
92
|
+
*
|
93
|
+
* @param selector selector of the function which corresponding access role was updated
|
94
|
+
* @param role effective required role to execute the function defined by the selector
|
95
|
+
*/
|
96
|
+
event AccessRoleUpdated(bytes4 selector, uint256 role);
|
97
|
+
|
98
|
+
/**
|
99
|
+
* @dev Logs function execution result on the target if the execution completed successfully
|
100
|
+
*
|
101
|
+
* @param selector selector of the function which was executed on the target contract
|
102
|
+
* @param data full calldata payload passed to the target contract (includes the 4-bytes selector)
|
103
|
+
* @param result execution response from the target contract
|
104
|
+
*/
|
105
|
+
event ExecutionComplete(bytes4 selector, bytes data, bytes result);
|
106
|
+
|
107
|
+
/**
|
108
|
+
* @dev Deploys an AccessControl Adapter binding it to the target OZ Ownable contract,
|
109
|
+
* and setting the ownership of the adapter itself to the deployer
|
110
|
+
*
|
111
|
+
* @param _target target OZ Ownable contract address
|
112
|
+
* @param _owner smart contract owner having full privileges
|
113
|
+
*/
|
114
|
+
constructor(address _target, address _owner) public AccessControl(_owner, 0) {
|
115
|
+
// verify the inputs
|
116
|
+
require(_target != address(0), "zero address");
|
117
|
+
|
118
|
+
// initialize internal contract state
|
119
|
+
target = _target;
|
120
|
+
}
|
121
|
+
|
122
|
+
/**
|
123
|
+
* @dev Updates the access role required to execute the function defined by its signature
|
124
|
+
* on the target contract
|
125
|
+
*
|
126
|
+
* @dev More on function signatures and selectors: https://docs.soliditylang.org/en/develop/abi-spec.html
|
127
|
+
*
|
128
|
+
* @param signature function signature on the target contract, for example
|
129
|
+
* "transferOwnership(address)"
|
130
|
+
* @param role role required to execute this function, or zero to disable
|
131
|
+
* access to the specified function for everyone
|
132
|
+
*/
|
133
|
+
function updateAccessRole(string memory signature, uint256 role) public {
|
134
|
+
// delegate to `updateAccessRole(bytes4, uint256)`
|
135
|
+
updateAccessRole(bytes4(keccak256(bytes(signature))), role);
|
136
|
+
}
|
137
|
+
|
138
|
+
/**
|
139
|
+
* @dev Updates the access role required to execute the function defined by its selector
|
140
|
+
* on the target contract
|
141
|
+
*
|
142
|
+
* @dev More on function signatures and selectors: https://docs.soliditylang.org/en/develop/abi-spec.html
|
143
|
+
*
|
144
|
+
* @param selector function selector on the target contract, for example
|
145
|
+
* 0xf2fde38b selector corresponds to the "transferOwnership(address)" function
|
146
|
+
* @param role role required to execute this function, or zero to disable
|
147
|
+
* access to the specified function for everyone
|
148
|
+
*/
|
149
|
+
function updateAccessRole(bytes4 selector, uint256 role) public {
|
150
|
+
// verify the access permission
|
151
|
+
require(isSenderInRole(ROLE_ACCESS_ROLES_MANAGER), "access denied");
|
152
|
+
|
153
|
+
// update the function access role
|
154
|
+
accessRoles[selector] = role;
|
155
|
+
|
156
|
+
// emit an event
|
157
|
+
emit AccessRoleUpdated(selector, role);
|
158
|
+
}
|
159
|
+
|
160
|
+
/**
|
161
|
+
* @dev Low-level execute of the data calldata on the target contract
|
162
|
+
*
|
163
|
+
* @dev This function extracts the target function selector from the calldata specified
|
164
|
+
* and verifies transaction executor permission to access the function on the target
|
165
|
+
* using the `accessRoles` mapping
|
166
|
+
*
|
167
|
+
* @dev Throws if there is no `accessRoles` mapping configured for the function
|
168
|
+
* @dev Throws if transaction executor role doesn't contain the required role from `accessRoles` mapping
|
169
|
+
* @dev Throws if execution on the target returns an error
|
170
|
+
*
|
171
|
+
* @param data low-level calldata to be passed as is to the target contract for the execution
|
172
|
+
* @return the response from the target contract after the successful execution
|
173
|
+
*/
|
174
|
+
function execute(bytes memory data) public payable returns(bytes memory) {
|
175
|
+
// extract the selector (first 4 bytes as bytes4) using assembly
|
176
|
+
bytes4 selector;
|
177
|
+
assembly {
|
178
|
+
// load the first word after the length field
|
179
|
+
selector := mload(add(data, 32))
|
180
|
+
}
|
181
|
+
|
182
|
+
// zero data length means we're trying to execute the receive() function on
|
183
|
+
// the target and supply some ether to the target; in this case we don't need a security check
|
184
|
+
// if the data is present, we're executing some real function and must do a security check
|
185
|
+
if(data.length != 0) {
|
186
|
+
// determine the role required to access the function
|
187
|
+
uint256 roleRequired = accessRoles[selector];
|
188
|
+
|
189
|
+
// verify function access role was already set
|
190
|
+
require(roleRequired != 0, "access role not set");
|
191
|
+
|
192
|
+
// verify the access permission
|
193
|
+
require(isSenderInRole(roleRequired), "access denied");
|
194
|
+
}
|
195
|
+
|
196
|
+
// execute the call on the target
|
197
|
+
(bool success, bytes memory result) = address(target).call{value: msg.value}(data);
|
198
|
+
|
199
|
+
// verify the execution completed successfully
|
200
|
+
require(success, "execution failed");
|
201
|
+
|
202
|
+
// emit an event
|
203
|
+
emit ExecutionComplete(selector, data, result);
|
204
|
+
|
205
|
+
// return the result
|
206
|
+
return result;
|
207
|
+
}
|
208
|
+
|
209
|
+
/**
|
210
|
+
* @dev Proxies the ether sent to the AccessControl Adapter to the target contract
|
211
|
+
*
|
212
|
+
* @dev Throws if target contract doesn't have the default payable receiver, i.e. doesn't accept ether
|
213
|
+
*/
|
214
|
+
receive() external payable {
|
215
|
+
// delegate to `execute(bytes)`
|
216
|
+
execute(bytes(""));
|
217
|
+
}
|
218
|
+
|
219
|
+
/**
|
220
|
+
* @dev Calls the target contract with the calldata specified in the transaction
|
221
|
+
*
|
222
|
+
* @dev See `execute()` function for details
|
223
|
+
* @dev Use `execute()` function directly if the target contract function signature collides
|
224
|
+
* with any of the AccessControl Adapter functions signature
|
225
|
+
*/
|
226
|
+
fallback() external payable {
|
227
|
+
// msg.data contains full calldata: function selector + encoded function arguments (if any)
|
228
|
+
// delegate to `execute(bytes)`
|
229
|
+
execute(msg.data);
|
230
|
+
}
|
231
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
2
|
+
pragma solidity >=0.4.22;
|
3
|
+
|
4
|
+
import "../AccessControl.sol";
|
5
|
+
|
6
|
+
// Used in AccessControl tests to check if `isSenderInRole` works through the `restrictedTo` modifier
|
7
|
+
contract AccessControlMock is AccessControl {
|
8
|
+
uint32 public constant RESTRICTED_ROLE = 1;
|
9
|
+
event Restricted();
|
10
|
+
constructor(address _owner, uint256 _features) AccessControl(_owner, _features){}
|
11
|
+
function restricted() public restrictedTo(RESTRICTED_ROLE) {
|
12
|
+
emit Restricted();
|
13
|
+
}
|
14
|
+
}
|