@lazy-sol/access-control 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }