@lazy-sol/access-control 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }