@bloxchain/contracts 1.0.0-alpha
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.
- package/README.md +49 -0
- package/abi/BareBlox.abi.json +1341 -0
- package/abi/BaseStateMachine.abi.json +1308 -0
- package/abi/ControlBlox.abi.json +6210 -0
- package/abi/EngineBlox.abi.json +872 -0
- package/abi/GuardController.abi.json +3045 -0
- package/abi/IDefinition.abi.json +94 -0
- package/abi/RoleBlox.abi.json +4569 -0
- package/abi/RuntimeRBAC.abi.json +1857 -0
- package/abi/RuntimeRBACDefinitions.abi.json +133 -0
- package/abi/SecureBlox.abi.json +4085 -0
- package/abi/SecureOwnable.abi.json +4085 -0
- package/abi/SecureOwnableDefinitions.abi.json +354 -0
- package/abi/SimpleRWA20.abi.json +5545 -0
- package/abi/SimpleRWA20Definitions.abi.json +172 -0
- package/abi/SimpleVault.abi.json +5208 -0
- package/abi/SimpleVaultDefinitions.abi.json +250 -0
- package/contracts/core/access/RuntimeRBAC.sol +344 -0
- package/contracts/core/access/interface/IRuntimeRBAC.sol +108 -0
- package/contracts/core/access/lib/definitions/RuntimeRBACDefinitions.sol +168 -0
- package/contracts/core/base/BaseStateMachine.sol +834 -0
- package/contracts/core/base/interface/IBaseStateMachine.sol +153 -0
- package/contracts/core/execution/GuardController.sol +507 -0
- package/contracts/core/execution/interface/IGuardController.sol +120 -0
- package/contracts/core/execution/lib/definitions/GuardControllerDefinitions.sol +401 -0
- package/contracts/core/lib/EngineBlox.sol +2283 -0
- package/contracts/core/security/SecureOwnable.sol +419 -0
- package/contracts/core/security/interface/ISecureOwnable.sol +118 -0
- package/contracts/core/security/lib/definitions/SecureOwnableDefinitions.sol +757 -0
- package/contracts/interfaces/IDefinition.sol +40 -0
- package/contracts/interfaces/IEventForwarder.sol +33 -0
- package/contracts/interfaces/IOnActionHook.sol +79 -0
- package/contracts/utils/SharedValidation.sol +486 -0
- package/package.json +47 -0
|
@@ -0,0 +1,2283 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
pragma solidity 0.8.33;
|
|
3
|
+
|
|
4
|
+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
5
|
+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
6
|
+
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
|
|
7
|
+
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
|
|
8
|
+
|
|
9
|
+
// Local imports
|
|
10
|
+
import "../../utils/SharedValidation.sol";
|
|
11
|
+
import "../../interfaces/IEventForwarder.sol";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @title EngineBlox
|
|
15
|
+
* @dev A library for implementing secure state abstraction with time-locks and meta-transactions
|
|
16
|
+
*
|
|
17
|
+
* This library provides a comprehensive framework for creating secure operations that require
|
|
18
|
+
* state management and multiple phases of approval before execution. It supports:
|
|
19
|
+
*
|
|
20
|
+
* - Time-locked operations that can only be executed after a waiting period
|
|
21
|
+
* - Meta-transactions for delegated approvals
|
|
22
|
+
* - Role-based access control for different operation types
|
|
23
|
+
* - Multiple execution types (standard function calls or raw transaction data)
|
|
24
|
+
* - Payment handling for both native tokens and ERC20 tokens
|
|
25
|
+
* - State machine-driven operation workflows
|
|
26
|
+
*
|
|
27
|
+
* The library supports flexible configuration of operation types, function schemas, and role permissions
|
|
28
|
+
* through direct function calls without requiring external definition files.
|
|
29
|
+
*
|
|
30
|
+
* The library is designed to be used as a building block for secure smart contract systems
|
|
31
|
+
* that require high levels of security and flexibility through state abstraction.
|
|
32
|
+
*/
|
|
33
|
+
library EngineBlox {
|
|
34
|
+
// ============ VERSION INFORMATION ============
|
|
35
|
+
bytes32 public constant PROTOCOL_NAME_HASH = keccak256("Bloxchain");
|
|
36
|
+
uint8 public constant VERSION_MAJOR = 1;
|
|
37
|
+
uint8 public constant VERSION_MINOR = 0;
|
|
38
|
+
uint8 public constant VERSION_PATCH = 0;
|
|
39
|
+
|
|
40
|
+
// ============ SYSTEM SAFETY LIMITS ============
|
|
41
|
+
// These constants define the safety range limits for system operations
|
|
42
|
+
// to prevent gas exhaustion attacks. These are immutable system-wide limits.
|
|
43
|
+
|
|
44
|
+
/// @dev Maximum number of items allowed in batch operations (prevents gas exhaustion)
|
|
45
|
+
uint256 public constant MAX_BATCH_SIZE = 200;
|
|
46
|
+
|
|
47
|
+
/// @dev Maximum total number of roles allowed in the system (prevents gas exhaustion in permission checks)
|
|
48
|
+
uint256 public constant MAX_ROLES = 1000;
|
|
49
|
+
|
|
50
|
+
/// @dev Maximum number of hooks allowed per function selector (prevents gas exhaustion in hook execution)
|
|
51
|
+
uint256 public constant MAX_HOOKS_PER_SELECTOR = 100;
|
|
52
|
+
|
|
53
|
+
/// @dev Maximum total number of functions allowed in the system (prevents gas exhaustion in function operations)
|
|
54
|
+
uint256 public constant MAX_FUNCTIONS = 2000;
|
|
55
|
+
|
|
56
|
+
using MessageHashUtils for bytes32;
|
|
57
|
+
using SharedValidation for *;
|
|
58
|
+
using EnumerableSet for EnumerableSet.UintSet;
|
|
59
|
+
using EnumerableSet for EnumerableSet.Bytes32Set;
|
|
60
|
+
using EnumerableSet for EnumerableSet.AddressSet;
|
|
61
|
+
using SafeERC20 for IERC20;
|
|
62
|
+
|
|
63
|
+
enum TxStatus {
|
|
64
|
+
UNDEFINED,
|
|
65
|
+
PENDING,
|
|
66
|
+
EXECUTING,
|
|
67
|
+
PROCESSING_PAYMENT,
|
|
68
|
+
CANCELLED,
|
|
69
|
+
COMPLETED,
|
|
70
|
+
FAILED,
|
|
71
|
+
REJECTED
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
enum TxAction {
|
|
75
|
+
EXECUTE_TIME_DELAY_REQUEST,
|
|
76
|
+
EXECUTE_TIME_DELAY_APPROVE,
|
|
77
|
+
EXECUTE_TIME_DELAY_CANCEL,
|
|
78
|
+
SIGN_META_REQUEST_AND_APPROVE,
|
|
79
|
+
SIGN_META_APPROVE,
|
|
80
|
+
SIGN_META_CANCEL,
|
|
81
|
+
EXECUTE_META_REQUEST_AND_APPROVE,
|
|
82
|
+
EXECUTE_META_APPROVE,
|
|
83
|
+
EXECUTE_META_CANCEL
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
struct TxParams {
|
|
87
|
+
address requester;
|
|
88
|
+
address target;
|
|
89
|
+
uint256 value;
|
|
90
|
+
uint256 gasLimit;
|
|
91
|
+
bytes32 operationType;
|
|
92
|
+
bytes4 executionSelector;
|
|
93
|
+
bytes executionParams;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
struct MetaTxParams {
|
|
97
|
+
uint256 chainId;
|
|
98
|
+
uint256 nonce;
|
|
99
|
+
address handlerContract;
|
|
100
|
+
bytes4 handlerSelector;
|
|
101
|
+
TxAction action;
|
|
102
|
+
uint256 deadline;
|
|
103
|
+
uint256 maxGasPrice;
|
|
104
|
+
address signer;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
struct TxRecord {
|
|
108
|
+
uint256 txId;
|
|
109
|
+
uint256 releaseTime;
|
|
110
|
+
TxStatus status;
|
|
111
|
+
TxParams params;
|
|
112
|
+
bytes32 message;
|
|
113
|
+
bytes result;
|
|
114
|
+
PaymentDetails payment;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
struct MetaTransaction {
|
|
118
|
+
TxRecord txRecord;
|
|
119
|
+
MetaTxParams params;
|
|
120
|
+
bytes32 message;
|
|
121
|
+
bytes signature;
|
|
122
|
+
bytes data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
struct PaymentDetails {
|
|
126
|
+
address recipient;
|
|
127
|
+
uint256 nativeTokenAmount;
|
|
128
|
+
address erc20TokenAddress;
|
|
129
|
+
uint256 erc20TokenAmount;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
struct Role {
|
|
133
|
+
string roleName;
|
|
134
|
+
bytes32 roleHash;
|
|
135
|
+
EnumerableSet.AddressSet authorizedWallets;
|
|
136
|
+
mapping(bytes4 => FunctionPermission) functionPermissions;
|
|
137
|
+
EnumerableSet.Bytes32Set functionSelectorsSet;
|
|
138
|
+
uint256 maxWallets;
|
|
139
|
+
uint256 walletCount;
|
|
140
|
+
bool isProtected;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
struct FunctionPermission {
|
|
144
|
+
bytes4 functionSelector;
|
|
145
|
+
uint16 grantedActionsBitmap; // Bitmap for TxAction enum (9 bits max)
|
|
146
|
+
bytes4[] handlerForSelectors; // Array of execution selectors this function can access. If it contains functionSelector, this is an execution selector; otherwise, these are handler selectors pointing to execution selectors
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
struct FunctionSchema {
|
|
150
|
+
string functionSignature;
|
|
151
|
+
bytes4 functionSelector;
|
|
152
|
+
bytes32 operationType;
|
|
153
|
+
string operationName;
|
|
154
|
+
uint16 supportedActionsBitmap; // Bitmap for TxAction enum (9 bits max)
|
|
155
|
+
bool isProtected;
|
|
156
|
+
bytes4[] handlerForSelectors;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============ DEFINITION STRUCTS ============
|
|
160
|
+
|
|
161
|
+
struct SecureOperationState {
|
|
162
|
+
// ============ SYSTEM STATE ============
|
|
163
|
+
bool initialized;
|
|
164
|
+
uint256 txCounter;
|
|
165
|
+
uint256 timeLockPeriodSec;
|
|
166
|
+
|
|
167
|
+
// ============ TRANSACTION MANAGEMENT ============
|
|
168
|
+
mapping(uint256 => TxRecord) txRecords;
|
|
169
|
+
EnumerableSet.UintSet pendingTransactionsSet;
|
|
170
|
+
|
|
171
|
+
// ============ ROLE-BASED ACCESS CONTROL ============
|
|
172
|
+
mapping(bytes32 => Role) roles;
|
|
173
|
+
EnumerableSet.Bytes32Set supportedRolesSet;
|
|
174
|
+
// Reverse index for O(1) wallet-to-role lookup (optimization for gas efficiency)
|
|
175
|
+
mapping(address => EnumerableSet.Bytes32Set) walletRoles; // wallet => roles set
|
|
176
|
+
|
|
177
|
+
// ============ FUNCTION MANAGEMENT ============
|
|
178
|
+
mapping(bytes4 => FunctionSchema) functions;
|
|
179
|
+
EnumerableSet.Bytes32Set supportedFunctionsSet; // Using Bytes32Set for bytes4 selectors
|
|
180
|
+
EnumerableSet.Bytes32Set supportedOperationTypesSet;
|
|
181
|
+
|
|
182
|
+
// ============ META-TRANSACTION SUPPORT ============
|
|
183
|
+
mapping(address => uint256) signerNonces;
|
|
184
|
+
|
|
185
|
+
// ============ EVENT FORWARDING ============
|
|
186
|
+
address eventForwarder;
|
|
187
|
+
|
|
188
|
+
// ============ FUNCTION TARGET MANAGEMENT ============
|
|
189
|
+
// Per-function target whitelist (always enforced; address(this) is always allowed)
|
|
190
|
+
mapping(bytes4 => EnumerableSet.AddressSet) functionTargetWhitelist;
|
|
191
|
+
// Per-function target hooks (generic pipeline for hook setup)
|
|
192
|
+
mapping(bytes4 => EnumerableSet.AddressSet) functionTargetHooks;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
bytes32 constant OWNER_ROLE = keccak256(bytes("OWNER_ROLE"));
|
|
196
|
+
bytes32 constant BROADCASTER_ROLE = keccak256(bytes("BROADCASTER_ROLE"));
|
|
197
|
+
bytes32 constant RECOVERY_ROLE = keccak256(bytes("RECOVERY_ROLE"));
|
|
198
|
+
|
|
199
|
+
// Native token transfer selector (reserved signature unlikely to exist in real contracts)
|
|
200
|
+
bytes4 public constant NATIVE_TRANSFER_SELECTOR = bytes4(keccak256("__bloxchain_native_transfer__()"));
|
|
201
|
+
bytes32 public constant NATIVE_TRANSFER_OPERATION = keccak256("NATIVE_TRANSFER");
|
|
202
|
+
|
|
203
|
+
// Payment update selector (reserved signature for payment detail updates)
|
|
204
|
+
bytes4 public constant UPDATE_PAYMENT_SELECTOR = bytes4(keccak256("__bloxchain_update_payment__()"));
|
|
205
|
+
bytes32 public constant UPDATE_PAYMENT_OPERATION = keccak256("UPDATE_PAYMENT");
|
|
206
|
+
|
|
207
|
+
// EIP-712 Type Hashes
|
|
208
|
+
bytes32 private constant TYPE_HASH = keccak256("MetaTransaction(TxRecord txRecord,MetaTxParams params,bytes data)TxRecord(uint256 txId,uint256 releaseTime,uint8 status,TxParams params,bytes32 message,bytes result,PaymentDetails payment)TxParams(address requester,address target,uint256 value,uint256 gasLimit,bytes32 operationType,bytes4 executionSelector,bytes executionParams)MetaTxParams(uint256 chainId,uint256 nonce,address handlerContract,bytes4 handlerSelector,uint8 action,uint256 deadline,uint256 maxGasPrice,address signer)PaymentDetails(address recipient,uint256 nativeTokenAmount,address erc20TokenAddress,uint256 erc20TokenAmount)");
|
|
209
|
+
bytes32 private constant DOMAIN_SEPARATOR_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
event TransactionEvent(
|
|
213
|
+
uint256 indexed txId,
|
|
214
|
+
bytes4 indexed functionHash,
|
|
215
|
+
TxStatus status,
|
|
216
|
+
address indexed requester,
|
|
217
|
+
address target,
|
|
218
|
+
bytes32 operationType
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// ============ SYSTEM STATE FUNCTIONS ============
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @dev Initializes the SecureOperationState with the specified time lock period and roles.
|
|
225
|
+
* @param self The SecureOperationState to initialize.
|
|
226
|
+
* @param _timeLockPeriodSec The time lock period in seconds.
|
|
227
|
+
* @param _owner The address of the owner.
|
|
228
|
+
* @param _broadcaster The address of the broadcaster.
|
|
229
|
+
* @param _recovery The address of the recovery.
|
|
230
|
+
*/
|
|
231
|
+
function initialize(
|
|
232
|
+
SecureOperationState storage self,
|
|
233
|
+
address _owner,
|
|
234
|
+
address _broadcaster,
|
|
235
|
+
address _recovery,
|
|
236
|
+
uint256 _timeLockPeriodSec
|
|
237
|
+
) public {
|
|
238
|
+
if (self.initialized) revert SharedValidation.AlreadyInitialized();
|
|
239
|
+
SharedValidation.validateNotZeroAddress(_owner);
|
|
240
|
+
SharedValidation.validateNotZeroAddress(_broadcaster);
|
|
241
|
+
SharedValidation.validateNotZeroAddress(_recovery);
|
|
242
|
+
SharedValidation.validateTimeLockPeriod(_timeLockPeriodSec);
|
|
243
|
+
|
|
244
|
+
self.timeLockPeriodSec = _timeLockPeriodSec;
|
|
245
|
+
self.txCounter = 0;
|
|
246
|
+
|
|
247
|
+
// Create base roles first
|
|
248
|
+
// OWNER and RECOVERY remain single-wallet roles (maxWallets = 1)
|
|
249
|
+
// BROADCASTER is now a multi-wallet role with support for up to 3 wallets
|
|
250
|
+
createRole(self, "OWNER_ROLE", 1, true);
|
|
251
|
+
createRole(self, "BROADCASTER_ROLE", 3, true);
|
|
252
|
+
createRole(self, "RECOVERY_ROLE", 1, true);
|
|
253
|
+
|
|
254
|
+
// Add authorized wallets to roles
|
|
255
|
+
assignWallet(self, OWNER_ROLE, _owner);
|
|
256
|
+
assignWallet(self, BROADCASTER_ROLE, _broadcaster);
|
|
257
|
+
assignWallet(self, RECOVERY_ROLE, _recovery);
|
|
258
|
+
|
|
259
|
+
// Mark as initialized after successful setup
|
|
260
|
+
self.initialized = true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @dev Updates the time lock period for the SecureOperationState.
|
|
265
|
+
* @param self The SecureOperationState to modify.
|
|
266
|
+
* @param _newTimeLockPeriodSec The new time lock period in seconds.
|
|
267
|
+
*/
|
|
268
|
+
function updateTimeLockPeriod(SecureOperationState storage self, uint256 _newTimeLockPeriodSec) public {
|
|
269
|
+
SharedValidation.validateTimeLockPeriod(_newTimeLockPeriodSec);
|
|
270
|
+
self.timeLockPeriodSec = _newTimeLockPeriodSec;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============ TRANSACTION MANAGEMENT FUNCTIONS ============
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @dev Gets the transaction record by its ID.
|
|
277
|
+
* @param self The SecureOperationState to check.
|
|
278
|
+
* @param txId The ID of the transaction to check.
|
|
279
|
+
* @return The TxRecord associated with the transaction ID.
|
|
280
|
+
* @notice Access control should be enforced by the calling contract.
|
|
281
|
+
*/
|
|
282
|
+
function getTxRecord(SecureOperationState storage self, uint256 txId) public view returns (TxRecord memory) {
|
|
283
|
+
return self.txRecords[txId];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @dev Requests a transaction with the specified parameters.
|
|
288
|
+
* @param self The SecureOperationState to modify.
|
|
289
|
+
* @param requester The address of the requester.
|
|
290
|
+
* @param target The target contract address for the transaction.
|
|
291
|
+
* @param value The value to send with the transaction.
|
|
292
|
+
* @param gasLimit The gas limit for the transaction.
|
|
293
|
+
* @param operationType The type of operation.
|
|
294
|
+
* @param handlerSelector The function selector of the handler/request function.
|
|
295
|
+
* @param executionSelector The function selector to execute (NATIVE_TRANSFER_SELECTOR for simple native token transfers).
|
|
296
|
+
* @param executionParams The encoded parameters for the function (empty for simple native token transfers).
|
|
297
|
+
* @return The created TxRecord.
|
|
298
|
+
*/
|
|
299
|
+
function txRequest(
|
|
300
|
+
SecureOperationState storage self,
|
|
301
|
+
address requester,
|
|
302
|
+
address target,
|
|
303
|
+
uint256 value,
|
|
304
|
+
uint256 gasLimit,
|
|
305
|
+
bytes32 operationType,
|
|
306
|
+
bytes4 handlerSelector,
|
|
307
|
+
bytes4 executionSelector,
|
|
308
|
+
bytes memory executionParams
|
|
309
|
+
) public returns (TxRecord memory) {
|
|
310
|
+
// Validate both execution and handler selector permissions
|
|
311
|
+
_validateExecutionAndHandlerPermissions(self, msg.sender, executionSelector, handlerSelector, TxAction.EXECUTE_TIME_DELAY_REQUEST);
|
|
312
|
+
|
|
313
|
+
return _txRequest(
|
|
314
|
+
self,
|
|
315
|
+
requester,
|
|
316
|
+
target,
|
|
317
|
+
value,
|
|
318
|
+
gasLimit,
|
|
319
|
+
operationType,
|
|
320
|
+
executionSelector,
|
|
321
|
+
executionParams
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @dev Internal helper function to request a transaction without permission checks.
|
|
327
|
+
* @param self The SecureOperationState to modify.
|
|
328
|
+
* @param requester The address of the requester.
|
|
329
|
+
* @param target The target contract address for the transaction.
|
|
330
|
+
* @param value The value to send with the transaction.
|
|
331
|
+
* @param gasLimit The gas limit for the transaction.
|
|
332
|
+
* @param operationType The type of operation.
|
|
333
|
+
* @param executionSelector The function selector to execute (NATIVE_TRANSFER_SELECTOR for simple native token transfers).
|
|
334
|
+
* @param executionParams The encoded parameters for the function (empty for simple native token transfers).
|
|
335
|
+
* @return The created TxRecord.
|
|
336
|
+
* @notice This function skips permission validation and should only be called from functions
|
|
337
|
+
* that have already validated permissions.
|
|
338
|
+
*/
|
|
339
|
+
function _txRequest(
|
|
340
|
+
SecureOperationState storage self,
|
|
341
|
+
address requester,
|
|
342
|
+
address target,
|
|
343
|
+
uint256 value,
|
|
344
|
+
uint256 gasLimit,
|
|
345
|
+
bytes32 operationType,
|
|
346
|
+
bytes4 executionSelector,
|
|
347
|
+
bytes memory executionParams
|
|
348
|
+
) private returns (TxRecord memory) {
|
|
349
|
+
SharedValidation.validateNotZeroAddress(target);
|
|
350
|
+
// enforce that the requested target is whitelisted for this selector.
|
|
351
|
+
_validateFunctionTargetWhitelist(self, executionSelector, target);
|
|
352
|
+
|
|
353
|
+
TxRecord memory txRequestRecord = createNewTxRecord(
|
|
354
|
+
self,
|
|
355
|
+
requester,
|
|
356
|
+
target,
|
|
357
|
+
value,
|
|
358
|
+
gasLimit,
|
|
359
|
+
operationType,
|
|
360
|
+
executionSelector,
|
|
361
|
+
executionParams
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
self.txRecords[txRequestRecord.txId] = txRequestRecord;
|
|
365
|
+
self.txCounter++;
|
|
366
|
+
|
|
367
|
+
// Add to pending transactions list
|
|
368
|
+
addToPendingTransactionsList(self, txRequestRecord.txId);
|
|
369
|
+
|
|
370
|
+
logTxEvent(self, txRequestRecord.txId, executionSelector);
|
|
371
|
+
|
|
372
|
+
return txRequestRecord;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @dev Approves a pending transaction after the release time.
|
|
377
|
+
* @param self The SecureOperationState to modify.
|
|
378
|
+
* @param txId The ID of the transaction to approve.
|
|
379
|
+
* @param handlerSelector The function selector of the handler/approval function.
|
|
380
|
+
* @return The updated TxRecord.
|
|
381
|
+
*/
|
|
382
|
+
function txDelayedApproval(
|
|
383
|
+
SecureOperationState storage self,
|
|
384
|
+
uint256 txId,
|
|
385
|
+
bytes4 handlerSelector
|
|
386
|
+
) public returns (TxRecord memory) {
|
|
387
|
+
// Validate both execution and handler selector permissions
|
|
388
|
+
_validateExecutionAndHandlerPermissions(self, msg.sender, self.txRecords[txId].params.executionSelector, handlerSelector, TxAction.EXECUTE_TIME_DELAY_APPROVE);
|
|
389
|
+
_validateTxStatus(self, txId, TxStatus.PENDING);
|
|
390
|
+
SharedValidation.validateReleaseTime(self.txRecords[txId].releaseTime);
|
|
391
|
+
|
|
392
|
+
// EFFECT: Update status to EXECUTING before external call to prevent reentrancy
|
|
393
|
+
self.txRecords[txId].status = TxStatus.EXECUTING;
|
|
394
|
+
|
|
395
|
+
// INTERACT: External call after state update
|
|
396
|
+
(bool success, bytes memory result) = executeTransaction(self, self.txRecords[txId]);
|
|
397
|
+
|
|
398
|
+
_completeTransaction(self, txId, success, result);
|
|
399
|
+
return self.txRecords[txId];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @dev Cancels a pending transaction.
|
|
404
|
+
* @param self The SecureOperationState to modify.
|
|
405
|
+
* @param txId The ID of the transaction to cancel.
|
|
406
|
+
* @param handlerSelector The function selector of the handler/cancellation function.
|
|
407
|
+
* @return The updated TxRecord.
|
|
408
|
+
*/
|
|
409
|
+
function txCancellation(
|
|
410
|
+
SecureOperationState storage self,
|
|
411
|
+
uint256 txId,
|
|
412
|
+
bytes4 handlerSelector
|
|
413
|
+
) public returns (TxRecord memory) {
|
|
414
|
+
// Validate both execution and handler selector permissions
|
|
415
|
+
_validateExecutionAndHandlerPermissions(self, msg.sender, self.txRecords[txId].params.executionSelector, handlerSelector, TxAction.EXECUTE_TIME_DELAY_CANCEL);
|
|
416
|
+
_validateTxStatus(self, txId, TxStatus.PENDING);
|
|
417
|
+
|
|
418
|
+
_cancelTransaction(self, txId);
|
|
419
|
+
|
|
420
|
+
return self.txRecords[txId];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* @dev Cancels a pending transaction using a meta-transaction.
|
|
425
|
+
* @param self The SecureOperationState to modify.
|
|
426
|
+
* @param metaTx The meta-transaction containing the signature and nonce.
|
|
427
|
+
* @return The updated TxRecord.
|
|
428
|
+
*/
|
|
429
|
+
function txCancellationWithMetaTx(SecureOperationState storage self, MetaTransaction memory metaTx) public returns (TxRecord memory) {
|
|
430
|
+
uint256 txId = metaTx.txRecord.txId;
|
|
431
|
+
// Validate both execution and handler selector permissions
|
|
432
|
+
_validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_CANCEL);
|
|
433
|
+
_validateTxStatus(self, txId, TxStatus.PENDING);
|
|
434
|
+
if (!verifySignature(self, metaTx)) revert SharedValidation.InvalidSignature(metaTx.signature);
|
|
435
|
+
|
|
436
|
+
incrementSignerNonce(self, metaTx.params.signer);
|
|
437
|
+
_cancelTransaction(self, txId);
|
|
438
|
+
|
|
439
|
+
return self.txRecords[txId];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* @dev Approves a pending transaction immediately using a meta-transaction.
|
|
444
|
+
* @param self The SecureOperationState to modify.
|
|
445
|
+
* @param metaTx The meta-transaction containing the signature and nonce.
|
|
446
|
+
* @return The updated TxRecord.
|
|
447
|
+
*/
|
|
448
|
+
function txApprovalWithMetaTx(SecureOperationState storage self, MetaTransaction memory metaTx) public returns (TxRecord memory) {
|
|
449
|
+
// Validate both execution and handler selector permissions
|
|
450
|
+
_validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_APPROVE);
|
|
451
|
+
|
|
452
|
+
return _txApprovalWithMetaTx(self, metaTx);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @dev Internal helper function to approve a pending transaction using a meta-transaction without permission checks.
|
|
457
|
+
* @param self The SecureOperationState to modify.
|
|
458
|
+
* @param metaTx The meta-transaction containing the signature and nonce.
|
|
459
|
+
* @return The updated TxRecord.
|
|
460
|
+
* @notice This function skips permission validation and should only be called from functions
|
|
461
|
+
* that have already validated permissions.
|
|
462
|
+
*/
|
|
463
|
+
function _txApprovalWithMetaTx(SecureOperationState storage self, MetaTransaction memory metaTx) private returns (TxRecord memory) {
|
|
464
|
+
uint256 txId = metaTx.txRecord.txId;
|
|
465
|
+
_validateTxStatus(self, txId, TxStatus.PENDING);
|
|
466
|
+
if (!verifySignature(self, metaTx)) revert SharedValidation.InvalidSignature(metaTx.signature);
|
|
467
|
+
|
|
468
|
+
incrementSignerNonce(self, metaTx.params.signer);
|
|
469
|
+
|
|
470
|
+
// EFFECT: Update status to EXECUTING before external call to prevent reentrancy
|
|
471
|
+
self.txRecords[txId].status = TxStatus.EXECUTING;
|
|
472
|
+
|
|
473
|
+
// INTERACT: External call after state update
|
|
474
|
+
(bool success, bytes memory result) = executeTransaction(self, self.txRecords[txId]);
|
|
475
|
+
|
|
476
|
+
_completeTransaction(self, txId, success, result);
|
|
477
|
+
|
|
478
|
+
return self.txRecords[txId];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* @dev Requests and immediately approves a transaction.
|
|
483
|
+
* @param self The SecureOperationState to modify.
|
|
484
|
+
* @param metaTx The meta-transaction containing the signature and nonce.
|
|
485
|
+
* @return The updated TxRecord.
|
|
486
|
+
*/
|
|
487
|
+
function requestAndApprove(
|
|
488
|
+
SecureOperationState storage self,
|
|
489
|
+
MetaTransaction memory metaTx
|
|
490
|
+
) public returns (TxRecord memory) {
|
|
491
|
+
// Validate both execution and handler selector permissions
|
|
492
|
+
_validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_REQUEST_AND_APPROVE);
|
|
493
|
+
|
|
494
|
+
TxRecord memory txRecord = _txRequest(
|
|
495
|
+
self,
|
|
496
|
+
metaTx.txRecord.params.requester,
|
|
497
|
+
metaTx.txRecord.params.target,
|
|
498
|
+
metaTx.txRecord.params.value,
|
|
499
|
+
metaTx.txRecord.params.gasLimit,
|
|
500
|
+
metaTx.txRecord.params.operationType,
|
|
501
|
+
metaTx.txRecord.params.executionSelector,
|
|
502
|
+
metaTx.txRecord.params.executionParams
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
metaTx.txRecord = txRecord;
|
|
506
|
+
return _txApprovalWithMetaTx(self, metaTx);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @dev Executes a transaction based on its execution type and attached payment.
|
|
511
|
+
* @param self The SecureOperationState storage reference (for validation)
|
|
512
|
+
* @param record The transaction record to execute.
|
|
513
|
+
* @return A tuple containing the success status and result of the execution.
|
|
514
|
+
* @custom:security REENTRANCY PROTECTION: This function is protected against reentrancy
|
|
515
|
+
* through a state machine pattern:
|
|
516
|
+
* 1. Entry functions (txDelayedApproval, txApprovalWithMetaTx) set status to EXECUTING
|
|
517
|
+
* BEFORE calling this function (Checks-Effects-Interactions pattern)
|
|
518
|
+
* 2. _validateTxExecuting ensures transaction is in EXECUTING status at entry
|
|
519
|
+
* 3. All reentry attempts would require PENDING status, but status is EXECUTING,
|
|
520
|
+
* causing _validateTxPending to revert in entry functions
|
|
521
|
+
* 4. Status flow is one-way: PENDING → EXECUTING → (COMPLETED/FAILED)
|
|
522
|
+
* This creates an effective reentrancy guard without additional storage overhead.
|
|
523
|
+
*/
|
|
524
|
+
function executeTransaction(SecureOperationState storage self, TxRecord memory record) private returns (bool, bytes memory) {
|
|
525
|
+
// Validate that transaction is in EXECUTING status (set by caller before this function)
|
|
526
|
+
// This proves reentrancy protection is active at entry point
|
|
527
|
+
_validateTxStatus(self, record.txId, TxStatus.EXECUTING);
|
|
528
|
+
|
|
529
|
+
bytes memory txData = prepareTransactionData(record);
|
|
530
|
+
uint gas = record.params.gasLimit;
|
|
531
|
+
if (gas == 0) {
|
|
532
|
+
gas = gasleft();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Execute the main transaction
|
|
536
|
+
// REENTRANCY SAFE: Status is EXECUTING, preventing reentry through entry functions
|
|
537
|
+
// that require PENDING status. Any reentry attempt would fail at _validateTxStatus(..., PENDING).
|
|
538
|
+
(bool success, bytes memory result) = record.params.target.call{value: record.params.value, gas: gas}(
|
|
539
|
+
txData
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
if (success) {
|
|
543
|
+
record.status = TxStatus.COMPLETED;
|
|
544
|
+
record.result = result;
|
|
545
|
+
|
|
546
|
+
// Execute attached payment if transaction was successful
|
|
547
|
+
if (record.payment.recipient != address(0)) {
|
|
548
|
+
executeAttachedPayment(self, record);
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
record.status = TxStatus.FAILED;
|
|
552
|
+
record.result = result;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return (success, result);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* @dev Executes the payment attached to a transaction record
|
|
560
|
+
* @param self The SecureOperationState storage reference (for validation)
|
|
561
|
+
* @param record The transaction record containing payment details
|
|
562
|
+
* @custom:security REENTRANCY PROTECTION: This function is protected by the same state machine
|
|
563
|
+
* pattern as executeTransaction:
|
|
564
|
+
* 1. Transaction status is EXECUTING (validated at entry)
|
|
565
|
+
* 2. Status changes to PROCESSING_PAYMENT before external calls
|
|
566
|
+
* 3. Reentry attempts would require PENDING status, which is impossible
|
|
567
|
+
* since status can only move forward: PENDING → EXECUTING → PROCESSING_PAYMENT
|
|
568
|
+
* 4. All entry functions check for PENDING status first, so reentry fails
|
|
569
|
+
* The external calls (native token transfer, ERC20 transfer) cannot reenter
|
|
570
|
+
* critical functions because the transaction is no longer in PENDING state.
|
|
571
|
+
*/
|
|
572
|
+
function executeAttachedPayment(
|
|
573
|
+
SecureOperationState storage self,
|
|
574
|
+
TxRecord memory record
|
|
575
|
+
) private {
|
|
576
|
+
// Validate that transaction is still in EXECUTING status
|
|
577
|
+
// This ensures reentrancy protection is maintained throughout payment execution
|
|
578
|
+
_validateTxStatus(self, record.txId, TxStatus.EXECUTING);
|
|
579
|
+
self.txRecords[record.txId].status = TxStatus.PROCESSING_PAYMENT;
|
|
580
|
+
|
|
581
|
+
PaymentDetails memory payment = record.payment;
|
|
582
|
+
|
|
583
|
+
// Execute native token payment if specified
|
|
584
|
+
if (payment.nativeTokenAmount > 0) {
|
|
585
|
+
if (address(this).balance < payment.nativeTokenAmount) {
|
|
586
|
+
revert SharedValidation.InsufficientBalance(address(this).balance, payment.nativeTokenAmount);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// REENTRANCY SAFE: Status is PROCESSING_PAYMENT, preventing reentry
|
|
590
|
+
// through functions that require PENDING status
|
|
591
|
+
(bool success, bytes memory result) = payment.recipient.call{value: payment.nativeTokenAmount}("");
|
|
592
|
+
if (!success) {
|
|
593
|
+
revert SharedValidation.PaymentFailed(payment.recipient, payment.nativeTokenAmount, result);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Execute ERC20 token payment if specified
|
|
598
|
+
if (payment.erc20TokenAmount > 0) {
|
|
599
|
+
SharedValidation.validateNotZeroAddress(payment.erc20TokenAddress);
|
|
600
|
+
|
|
601
|
+
IERC20 erc20Token = IERC20(payment.erc20TokenAddress);
|
|
602
|
+
if (erc20Token.balanceOf(address(this)) < payment.erc20TokenAmount) {
|
|
603
|
+
revert SharedValidation.InsufficientBalance(erc20Token.balanceOf(address(this)), payment.erc20TokenAmount);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// REENTRANCY SAFE: Status is PROCESSING_PAYMENT, preventing reentry
|
|
607
|
+
// through functions that require PENDING status. safeTransfer uses
|
|
608
|
+
// SafeERC20 which includes reentrancy protection, but our state machine
|
|
609
|
+
// provides additional defense-in-depth protection.
|
|
610
|
+
erc20Token.safeTransfer(payment.recipient, payment.erc20TokenAmount);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* @dev Prepares transaction data from execution selector and params without executing it.
|
|
616
|
+
* @param record The transaction record to prepare data for.
|
|
617
|
+
* @return The prepared transaction data.
|
|
618
|
+
*/
|
|
619
|
+
function prepareTransactionData(TxRecord memory record) private pure returns (bytes memory) {
|
|
620
|
+
// If executionSelector is NATIVE_TRANSFER_SELECTOR, it's a simple native token transfer (no function call)
|
|
621
|
+
if (record.params.executionSelector == NATIVE_TRANSFER_SELECTOR) {
|
|
622
|
+
// SECURITY: Validate empty params to prevent confusion with real function calls
|
|
623
|
+
if (record.params.executionParams.length != 0) {
|
|
624
|
+
revert SharedValidation.NotSupported();
|
|
625
|
+
}
|
|
626
|
+
return ""; // Empty calldata for native token transfer
|
|
627
|
+
}
|
|
628
|
+
// Otherwise, encode the function selector with params
|
|
629
|
+
// For low-level calls, we need: selector (4 bytes) + ABI-encoded params
|
|
630
|
+
// abi.encodePacked concatenates bytes4 and bytes memory correctly
|
|
631
|
+
return abi.encodePacked(record.params.executionSelector, record.params.executionParams);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* @notice Creates a new transaction record with basic fields populated
|
|
637
|
+
* @dev Initializes a TxRecord struct with the provided parameters and default values
|
|
638
|
+
* @param self The SecureOperationState to reference for txId and timelock
|
|
639
|
+
* @param requester The address initiating the transaction
|
|
640
|
+
* @param target The contract address that will receive the transaction
|
|
641
|
+
* @param value The amount of native tokens to send with the transaction
|
|
642
|
+
* @param gasLimit The maximum gas allowed for the transaction
|
|
643
|
+
* @param operationType The type of operation being performed
|
|
644
|
+
* @param executionSelector The function selector to execute (NATIVE_TRANSFER_SELECTOR for simple native token transfers)
|
|
645
|
+
* @param executionParams The encoded parameters for the function (empty for simple native token transfers)
|
|
646
|
+
* @return TxRecord A new transaction record with populated fields
|
|
647
|
+
*/
|
|
648
|
+
function createNewTxRecord(
|
|
649
|
+
SecureOperationState storage self,
|
|
650
|
+
address requester,
|
|
651
|
+
address target,
|
|
652
|
+
uint256 value,
|
|
653
|
+
uint256 gasLimit,
|
|
654
|
+
bytes32 operationType,
|
|
655
|
+
bytes4 executionSelector,
|
|
656
|
+
bytes memory executionParams
|
|
657
|
+
) private view returns (TxRecord memory) {
|
|
658
|
+
return TxRecord({
|
|
659
|
+
txId: self.txCounter + 1,
|
|
660
|
+
releaseTime: block.timestamp + self.timeLockPeriodSec * 1 seconds,
|
|
661
|
+
status: TxStatus.PENDING,
|
|
662
|
+
params: TxParams({
|
|
663
|
+
requester: requester,
|
|
664
|
+
target: target,
|
|
665
|
+
value: value,
|
|
666
|
+
gasLimit: gasLimit,
|
|
667
|
+
operationType: operationType,
|
|
668
|
+
executionSelector: executionSelector,
|
|
669
|
+
executionParams: executionParams
|
|
670
|
+
}),
|
|
671
|
+
message: 0,
|
|
672
|
+
result: "",
|
|
673
|
+
payment: PaymentDetails({
|
|
674
|
+
recipient: address(0),
|
|
675
|
+
nativeTokenAmount: 0,
|
|
676
|
+
erc20TokenAddress: address(0),
|
|
677
|
+
erc20TokenAmount: 0
|
|
678
|
+
})
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* @dev Adds a transaction ID to the pending transactions set.
|
|
684
|
+
* @param self The SecureOperationState to modify.
|
|
685
|
+
* @param txId The transaction ID to add to the pending set.
|
|
686
|
+
*/
|
|
687
|
+
function addToPendingTransactionsList(SecureOperationState storage self, uint256 txId) private {
|
|
688
|
+
SharedValidation.validateTransactionExists(txId);
|
|
689
|
+
_validateTxStatus(self, txId, TxStatus.PENDING);
|
|
690
|
+
|
|
691
|
+
// Try to add transaction ID to the set - add() returns false if already exists
|
|
692
|
+
if (!self.pendingTransactionsSet.add(txId)) {
|
|
693
|
+
revert SharedValidation.ResourceAlreadyExists(bytes32(uint256(txId)));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* @dev Removes a transaction ID from the pending transactions set.
|
|
699
|
+
* @param self The SecureOperationState to modify.
|
|
700
|
+
* @param txId The transaction ID to remove from the pending set.
|
|
701
|
+
*/
|
|
702
|
+
function removeFromPendingTransactionsList(SecureOperationState storage self, uint256 txId) private {
|
|
703
|
+
SharedValidation.validateTransactionExists(txId);
|
|
704
|
+
|
|
705
|
+
// Remove the transaction ID from the set (O(1) operation)
|
|
706
|
+
if (!self.pendingTransactionsSet.remove(txId)) {
|
|
707
|
+
revert SharedValidation.ResourceNotFound(bytes32(uint256(txId)));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ============ PAYMENT MANAGEMENT FUNCTIONS ============
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* @dev Updates payment details for a pending transaction
|
|
715
|
+
* @param self The SecureOperationState to modify
|
|
716
|
+
* @param txId The transaction ID to update payment for
|
|
717
|
+
* @param paymentDetails The new payment details
|
|
718
|
+
* @notice Access control: Requires permission for UPDATE_PAYMENT_SELECTOR and execution selector
|
|
719
|
+
* @notice This prevents attackers from redirecting funds after transaction request
|
|
720
|
+
* @notice Contracts must register UPDATE_PAYMENT_SELECTOR schema and grant permissions
|
|
721
|
+
* @notice Permission check: Both UPDATE_PAYMENT_SELECTOR AND execution selector permissions required
|
|
722
|
+
*/
|
|
723
|
+
function updatePaymentForTransaction(
|
|
724
|
+
SecureOperationState storage self,
|
|
725
|
+
uint256 txId,
|
|
726
|
+
PaymentDetails memory paymentDetails
|
|
727
|
+
) public {
|
|
728
|
+
_validateTxStatus(self, txId, TxStatus.PENDING);
|
|
729
|
+
|
|
730
|
+
// Permission-based access control using macro selector
|
|
731
|
+
// Requires permission for UPDATE_PAYMENT_SELECTOR with EXECUTE_TIME_DELAY_REQUEST action
|
|
732
|
+
if (!hasActionPermission(self, msg.sender, UPDATE_PAYMENT_SELECTOR, TxAction.EXECUTE_TIME_DELAY_REQUEST)) {
|
|
733
|
+
revert SharedValidation.NoPermission(msg.sender);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Also verify permission for the transaction's execution selector
|
|
737
|
+
// This ensures caller has permission for the underlying transaction (dual permission check)
|
|
738
|
+
if (!hasActionPermission(self, msg.sender, self.txRecords[txId].params.executionSelector, TxAction.EXECUTE_TIME_DELAY_REQUEST)) {
|
|
739
|
+
revert SharedValidation.NoPermission(msg.sender);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
self.txRecords[txId].payment = paymentDetails;
|
|
743
|
+
|
|
744
|
+
logTxEvent(self, txId, self.txRecords[txId].params.executionSelector);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ============ ROLE-BASED ACCESS CONTROL FUNCTIONS ============
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* @dev Gets the role by its hash.
|
|
752
|
+
* @param self The SecureOperationState to check.
|
|
753
|
+
* @param role The role to get the hash for.
|
|
754
|
+
* @return The role associated with the hash, or Role(0) if the role doesn't exist.
|
|
755
|
+
* @notice Access control should be enforced by the calling contract.
|
|
756
|
+
*/
|
|
757
|
+
function getRole(SecureOperationState storage self, bytes32 role) public view returns (Role storage) {
|
|
758
|
+
_validateRoleExists(self, role);
|
|
759
|
+
return self.roles[role];
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* @dev Creates a role with specified function permissions.
|
|
764
|
+
* @param self The SecureOperationState to check.
|
|
765
|
+
* @param roleName Name of the role.
|
|
766
|
+
* @param maxWallets Maximum number of wallets allowed for this role.
|
|
767
|
+
* @param isProtected Whether the role is protected from removal.
|
|
768
|
+
*/
|
|
769
|
+
function createRole(
|
|
770
|
+
SecureOperationState storage self,
|
|
771
|
+
string memory roleName,
|
|
772
|
+
uint256 maxWallets,
|
|
773
|
+
bool isProtected
|
|
774
|
+
) public {
|
|
775
|
+
bytes32 roleHash = keccak256(bytes(roleName));
|
|
776
|
+
|
|
777
|
+
// Validate role count limit
|
|
778
|
+
SharedValidation.validateRoleCount(
|
|
779
|
+
self.supportedRolesSet.length(),
|
|
780
|
+
MAX_ROLES
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
// Check if role already exists in mapping - if so, revert
|
|
784
|
+
if (self.roles[roleHash].roleHash == roleHash) {
|
|
785
|
+
revert SharedValidation.ResourceAlreadyExists(roleHash);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Add the role to the set - if it already exists, revert to prevent inconsistent state
|
|
789
|
+
if (!self.supportedRolesSet.add(roleHash)) {
|
|
790
|
+
revert SharedValidation.ResourceAlreadyExists(roleHash);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Initialize the role mapping
|
|
794
|
+
self.roles[roleHash].roleName = roleName;
|
|
795
|
+
self.roles[roleHash].roleHash = roleHash;
|
|
796
|
+
self.roles[roleHash].maxWallets = maxWallets;
|
|
797
|
+
self.roles[roleHash].walletCount = 0;
|
|
798
|
+
self.roles[roleHash].isProtected = isProtected;
|
|
799
|
+
|
|
800
|
+
_validateRoleExists(self, roleHash);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* @dev Removes a role from the system.
|
|
805
|
+
* @param self The SecureOperationState to modify.
|
|
806
|
+
* @param roleHash The hash of the role to remove.
|
|
807
|
+
* @notice Security: Cannot remove protected roles to maintain system integrity.
|
|
808
|
+
*/
|
|
809
|
+
function removeRole(
|
|
810
|
+
SecureOperationState storage self,
|
|
811
|
+
bytes32 roleHash
|
|
812
|
+
) public {
|
|
813
|
+
// Validate that the role exists (checks both roles mapping and supportedRolesSet)
|
|
814
|
+
_validateRoleExists(self, roleHash);
|
|
815
|
+
|
|
816
|
+
// Security check: Prevent removing protected roles
|
|
817
|
+
if (self.roles[roleHash].isProtected) {
|
|
818
|
+
revert SharedValidation.CannotModifyProtected(roleHash);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
Role storage roleData = self.roles[roleHash];
|
|
822
|
+
|
|
823
|
+
// Clean up reverse index for all wallets in this role
|
|
824
|
+
// Collect all wallets first (to avoid modifying set during iteration)
|
|
825
|
+
uint256 walletCount = roleData.authorizedWallets.length();
|
|
826
|
+
address[] memory wallets = new address[](walletCount);
|
|
827
|
+
|
|
828
|
+
for (uint256 i = 0; i < walletCount; i++) {
|
|
829
|
+
wallets[i] = roleData.authorizedWallets.at(i);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Remove role from each wallet's reverse index
|
|
833
|
+
// This ensures the wallet-to-role index remains consistent for O(1) permission checks
|
|
834
|
+
for (uint256 i = 0; i < walletCount; i++) {
|
|
835
|
+
self.walletRoles[wallets[i]].remove(roleHash);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Clear the role data from roles mapping
|
|
839
|
+
// Remove the role from the supported roles set (O(1) operation)
|
|
840
|
+
// NOTE: Mappings (functionPermissions, authorizedWallets, functionSelectorsSet)
|
|
841
|
+
// are not deleted by Solidity's delete operator. This is acceptable because:
|
|
842
|
+
// 1. Role is removed from supportedRolesSet, making it inaccessible via role queries
|
|
843
|
+
// 2. Reverse index (walletRoles) is cleaned up above, so permission checks won't find this role
|
|
844
|
+
// 3. All access checks use the reverse index (walletRoles) for O(1) lookups, so orphaned data is unreachable
|
|
845
|
+
// 4. Role recreation with same name would pass roleHash check but mappings
|
|
846
|
+
// would be effectively reset since role is reinitialized from scratch
|
|
847
|
+
delete self.roles[roleHash];
|
|
848
|
+
if (!self.supportedRolesSet.remove(roleHash)) {
|
|
849
|
+
revert SharedValidation.ResourceNotFound(roleHash);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* @dev Checks if a wallet is authorized for a role.
|
|
855
|
+
* @param self The SecureOperationState to check.
|
|
856
|
+
* @param roleHash The hash of the role to check.
|
|
857
|
+
* @param wallet The wallet address to check.
|
|
858
|
+
* @return True if the wallet is authorized for the role, false otherwise.
|
|
859
|
+
*/
|
|
860
|
+
function hasRole(SecureOperationState storage self, bytes32 roleHash, address wallet) public view returns (bool) {
|
|
861
|
+
Role storage role = getRole(self, roleHash);
|
|
862
|
+
return role.authorizedWallets.contains(wallet);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* @dev Adds a wallet address to a role in the roles mapping.
|
|
867
|
+
* @param self The SecureOperationState to modify.
|
|
868
|
+
* @param role The role hash to add the wallet to.
|
|
869
|
+
* @param wallet The wallet address to add.
|
|
870
|
+
*/
|
|
871
|
+
function assignWallet(SecureOperationState storage self, bytes32 role, address wallet) public {
|
|
872
|
+
SharedValidation.validateNotZeroAddress(wallet);
|
|
873
|
+
_validateRoleExists(self, role);
|
|
874
|
+
|
|
875
|
+
Role storage roleData = self.roles[role];
|
|
876
|
+
SharedValidation.validateWalletLimit(roleData.authorizedWallets.length(), roleData.maxWallets);
|
|
877
|
+
|
|
878
|
+
// Check if wallet is already in the role
|
|
879
|
+
if (roleData.authorizedWallets.contains(wallet)) revert SharedValidation.ItemAlreadyExists(wallet);
|
|
880
|
+
|
|
881
|
+
if (!roleData.authorizedWallets.add(wallet)) {
|
|
882
|
+
revert SharedValidation.ItemAlreadyExists(wallet);
|
|
883
|
+
}
|
|
884
|
+
roleData.walletCount = roleData.authorizedWallets.length();
|
|
885
|
+
|
|
886
|
+
// Update reverse index for O(1) permission checks
|
|
887
|
+
self.walletRoles[wallet].add(role);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* @dev Updates a role from an old address to a new address.
|
|
892
|
+
* @param self The SecureOperationState to modify.
|
|
893
|
+
* @param role The role to update.
|
|
894
|
+
* @param newWallet The new wallet address to assign the role to.
|
|
895
|
+
* @param oldWallet The old wallet address to remove from the role.
|
|
896
|
+
*/
|
|
897
|
+
function updateAssignedWallet(SecureOperationState storage self, bytes32 role, address newWallet, address oldWallet) public {
|
|
898
|
+
_validateRoleExists(self, role);
|
|
899
|
+
SharedValidation.validateNotZeroAddress(newWallet);
|
|
900
|
+
SharedValidation.validateNewAddress(newWallet, oldWallet);
|
|
901
|
+
|
|
902
|
+
// Check if old wallet exists in the role
|
|
903
|
+
Role storage roleData = self.roles[role];
|
|
904
|
+
|
|
905
|
+
// Remove old wallet
|
|
906
|
+
if (!roleData.authorizedWallets.remove(oldWallet)) {
|
|
907
|
+
revert SharedValidation.ItemNotFound(oldWallet);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Add new wallet (should always succeed since we verified it doesn't exist)
|
|
911
|
+
if (!roleData.authorizedWallets.add(newWallet)) {
|
|
912
|
+
revert SharedValidation.OperationFailed();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Update reverse indices for O(1) permission checks
|
|
916
|
+
self.walletRoles[oldWallet].remove(role);
|
|
917
|
+
self.walletRoles[newWallet].add(role);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* @dev Removes a wallet from a role.
|
|
922
|
+
* @param self The SecureOperationState to modify.
|
|
923
|
+
* @param role The role to remove the wallet from.
|
|
924
|
+
* @param wallet The wallet address to remove.
|
|
925
|
+
* @notice Security: Cannot remove the last wallet from a protected role
|
|
926
|
+
*/
|
|
927
|
+
function revokeWallet(SecureOperationState storage self, bytes32 role, address wallet) public {
|
|
928
|
+
_validateRoleExists(self, role);
|
|
929
|
+
|
|
930
|
+
Role storage roleData = self.roles[role];
|
|
931
|
+
|
|
932
|
+
// Security check: Prevent removing the last wallet from a protected role
|
|
933
|
+
if (roleData.isProtected && roleData.authorizedWallets.length() <= 1) {
|
|
934
|
+
revert SharedValidation.CannotModifyProtected(bytes32(role));
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Remove the wallet (O(1) operation)
|
|
938
|
+
if (!roleData.authorizedWallets.remove(wallet)) {
|
|
939
|
+
revert SharedValidation.ItemNotFound(wallet);
|
|
940
|
+
}
|
|
941
|
+
roleData.walletCount = roleData.authorizedWallets.length();
|
|
942
|
+
|
|
943
|
+
// Update reverse index for O(1) permission checks
|
|
944
|
+
self.walletRoles[wallet].remove(role);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* @dev Adds a function permission to an existing role.
|
|
949
|
+
* @param self The SecureOperationState to modify.
|
|
950
|
+
* @param roleHash The role hash to add the function permission to.
|
|
951
|
+
* @param functionPermission The function permission to add.
|
|
952
|
+
*/
|
|
953
|
+
function addFunctionToRole(
|
|
954
|
+
SecureOperationState storage self,
|
|
955
|
+
bytes32 roleHash,
|
|
956
|
+
FunctionPermission memory functionPermission
|
|
957
|
+
) public {
|
|
958
|
+
bytes32 functionSelectorHash = bytes32(functionPermission.functionSelector);
|
|
959
|
+
|
|
960
|
+
// Check if role exists (checks both roles mapping and supportedRolesSet)
|
|
961
|
+
_validateRoleExists(self, roleHash);
|
|
962
|
+
|
|
963
|
+
// Validate that all handlerForSelectors in permission are in the schema's handlerForSelectors array
|
|
964
|
+
_validateHandlerForSelectors(self, functionPermission.functionSelector, functionPermission.handlerForSelectors);
|
|
965
|
+
|
|
966
|
+
// Validate that all grantedActions are supported by the function
|
|
967
|
+
_validateMetaTxPermissions(self, functionPermission);
|
|
968
|
+
|
|
969
|
+
// add the function selector to the role's function selectors set and mapping
|
|
970
|
+
Role storage role = self.roles[roleHash];
|
|
971
|
+
role.functionPermissions[functionPermission.functionSelector] = functionPermission;
|
|
972
|
+
|
|
973
|
+
// Add to role's function selectors set
|
|
974
|
+
if (!role.functionSelectorsSet.add(functionSelectorHash)) {
|
|
975
|
+
revert SharedValidation.ResourceAlreadyExists(functionSelectorHash);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* @dev Removes a function permission from an existing role.
|
|
981
|
+
* @param self The SecureOperationState to modify.
|
|
982
|
+
* @param roleHash The role hash to remove the function permission from.
|
|
983
|
+
* @param functionSelector The function selector to remove from the role.
|
|
984
|
+
*/
|
|
985
|
+
function removeFunctionFromRole(
|
|
986
|
+
SecureOperationState storage self,
|
|
987
|
+
bytes32 roleHash,
|
|
988
|
+
bytes4 functionSelector
|
|
989
|
+
) public {
|
|
990
|
+
// Check if role exists (checks both roles mapping and supportedRolesSet)
|
|
991
|
+
_validateRoleExists(self, roleHash);
|
|
992
|
+
|
|
993
|
+
// Security check: Prevent removing protected functions from roles
|
|
994
|
+
// Check if function exists and is protected
|
|
995
|
+
if (self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
|
|
996
|
+
FunctionSchema memory functionSchema = self.functions[functionSelector];
|
|
997
|
+
if (functionSchema.isProtected) {
|
|
998
|
+
revert SharedValidation.CannotModifyProtected(bytes32(functionSelector));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Remove the function permission
|
|
1003
|
+
Role storage role = self.roles[roleHash];
|
|
1004
|
+
delete role.functionPermissions[functionSelector];
|
|
1005
|
+
if (!role.functionSelectorsSet.remove(bytes32(functionSelector))) {
|
|
1006
|
+
revert SharedValidation.ResourceNotFound(bytes32(functionSelector));
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* @dev Checks if a wallet has permission for a specific function and action.
|
|
1012
|
+
* @param self The SecureOperationState to check.
|
|
1013
|
+
* @param wallet The wallet address to check.
|
|
1014
|
+
* @param functionSelector The function selector to check permissions for.
|
|
1015
|
+
* @param requestedAction The specific action being requested.
|
|
1016
|
+
* @return True if the wallet has permission for the function and action, false otherwise.
|
|
1017
|
+
*/
|
|
1018
|
+
function hasActionPermission(
|
|
1019
|
+
SecureOperationState storage self,
|
|
1020
|
+
address wallet,
|
|
1021
|
+
bytes4 functionSelector,
|
|
1022
|
+
TxAction requestedAction
|
|
1023
|
+
) public view returns (bool) {
|
|
1024
|
+
// OPTIMIZED: Use reverse index instead of iterating all roles (O(n) -> O(1) lookup)
|
|
1025
|
+
// This provides significant gas savings when there are many roles
|
|
1026
|
+
EnumerableSet.Bytes32Set storage walletRolesSet = self.walletRoles[wallet];
|
|
1027
|
+
uint256 rolesLength = walletRolesSet.length();
|
|
1028
|
+
|
|
1029
|
+
for (uint i = 0; i < rolesLength; i++) {
|
|
1030
|
+
bytes32 roleHash = walletRolesSet.at(i);
|
|
1031
|
+
|
|
1032
|
+
// Use the dedicated role permission check function
|
|
1033
|
+
if (roleHasActionPermission(self, roleHash, functionSelector, requestedAction)) {
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* @dev Checks if a wallet has view permission for any role (privacy function access)
|
|
1042
|
+
* @param self The SecureOperationState to check.
|
|
1043
|
+
* @param wallet The wallet address to check.
|
|
1044
|
+
* @return True if the wallet has view permission, false otherwise.
|
|
1045
|
+
*/
|
|
1046
|
+
function hasAnyRole(
|
|
1047
|
+
SecureOperationState storage self,
|
|
1048
|
+
address wallet
|
|
1049
|
+
) public view returns (bool) {
|
|
1050
|
+
// OPTIMIZED: Use reverse index - O(1) check instead of O(n) iteration
|
|
1051
|
+
// This provides significant gas savings when there are many roles
|
|
1052
|
+
return self.walletRoles[wallet].length() > 0;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* @dev Checks if a specific role has permission for a function and action.
|
|
1057
|
+
* @param self The SecureOperationState to check.
|
|
1058
|
+
* @param roleHash The role hash to check.
|
|
1059
|
+
* @param functionSelector The function selector to check permissions for.
|
|
1060
|
+
* @param requestedAction The specific action being requested.
|
|
1061
|
+
* @return True if the role has permission for the function and action, false otherwise.
|
|
1062
|
+
*/
|
|
1063
|
+
function roleHasActionPermission(
|
|
1064
|
+
SecureOperationState storage self,
|
|
1065
|
+
bytes32 roleHash,
|
|
1066
|
+
bytes4 functionSelector,
|
|
1067
|
+
TxAction requestedAction
|
|
1068
|
+
) public view returns (bool) {
|
|
1069
|
+
Role storage role = self.roles[roleHash];
|
|
1070
|
+
|
|
1071
|
+
// Check if function has permissions
|
|
1072
|
+
if (!role.functionSelectorsSet.contains(bytes32(functionSelector))) {
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
FunctionPermission storage permission = role.functionPermissions[functionSelector];
|
|
1077
|
+
|
|
1078
|
+
return hasActionInBitmap(permission.grantedActionsBitmap, requestedAction);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// ============ FUNCTION MANAGEMENT FUNCTIONS ============
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* @dev Creates a function access control with specified permissions.
|
|
1085
|
+
* @param self The SecureOperationState to check.
|
|
1086
|
+
* @param functionSignature Function signature (e.g., "transfer(address,uint256)") or function name.
|
|
1087
|
+
* @param functionSelector Hash identifier for the function.
|
|
1088
|
+
* @param operationName The name of the operation type.
|
|
1089
|
+
* @param supportedActionsBitmap Bitmap of permissions required to execute this function.
|
|
1090
|
+
* @param isProtected Whether the function schema is protected from removal.
|
|
1091
|
+
* @param handlerForSelectors Non-empty array required - execution selectors must contain self-reference, handler selectors must point to execution selectors
|
|
1092
|
+
*/
|
|
1093
|
+
function createFunctionSchema(
|
|
1094
|
+
SecureOperationState storage self,
|
|
1095
|
+
string memory functionSignature,
|
|
1096
|
+
bytes4 functionSelector,
|
|
1097
|
+
string memory operationName,
|
|
1098
|
+
uint16 supportedActionsBitmap,
|
|
1099
|
+
bool isProtected,
|
|
1100
|
+
bytes4[] memory handlerForSelectors
|
|
1101
|
+
) public {
|
|
1102
|
+
// Validate that functionSignature matches functionSelector
|
|
1103
|
+
// Note: NATIVE_TRANSFER_SELECTOR uses a reserved signature that represents native token transfers
|
|
1104
|
+
// and doesn't correspond to a real function, but still requires signature validation
|
|
1105
|
+
bytes4 derivedSelector = bytes4(keccak256(bytes(functionSignature)));
|
|
1106
|
+
if (derivedSelector != functionSelector) {
|
|
1107
|
+
revert SharedValidation.FunctionSelectorMismatch(functionSelector, derivedSelector);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// SECURITY: Validate that functions existing in contract bytecode must be protected
|
|
1111
|
+
// This checks if the function selector exists in the contract's bytecode
|
|
1112
|
+
// If it exists, it must be protected to prevent accidental removal of system-critical functions
|
|
1113
|
+
_validateContractFunctionProtection(functionSignature, functionSelector, isProtected);
|
|
1114
|
+
|
|
1115
|
+
// Derive operation type from operation name
|
|
1116
|
+
bytes32 derivedOperationType = keccak256(bytes(operationName));
|
|
1117
|
+
|
|
1118
|
+
// Validate handlerForSelectors: non-empty and all selectors are non-zero
|
|
1119
|
+
// NOTE:
|
|
1120
|
+
// - Empty arrays are NOT allowed anymore. Execution selectors must have
|
|
1121
|
+
// at least one entry pointing to themselves (self-reference), and
|
|
1122
|
+
// handler selectors must point to valid execution selectors.
|
|
1123
|
+
// - bytes4(0) is never allowed in this array.
|
|
1124
|
+
if (handlerForSelectors.length == 0) {
|
|
1125
|
+
revert SharedValidation.OperationFailed();
|
|
1126
|
+
}
|
|
1127
|
+
for (uint256 i = 0; i < handlerForSelectors.length; i++) {
|
|
1128
|
+
if (handlerForSelectors[i] == bytes4(0)) {
|
|
1129
|
+
revert SharedValidation.ResourceNotFound(bytes32(0)); // Zero selector is invalid
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// register the operation type if it's not already in the set
|
|
1134
|
+
SharedValidation.validateOperationTypeNotZero(derivedOperationType);
|
|
1135
|
+
if (self.supportedOperationTypesSet.add(derivedOperationType)) {
|
|
1136
|
+
// do nothing
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Validate function count limit
|
|
1140
|
+
SharedValidation.validateFunctionCount(
|
|
1141
|
+
self.supportedFunctionsSet.length(),
|
|
1142
|
+
MAX_FUNCTIONS
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
// Check if function already exists in the set
|
|
1146
|
+
if (self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
|
|
1147
|
+
revert SharedValidation.ResourceAlreadyExists(bytes32(functionSelector));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
FunctionSchema storage schema = self.functions[functionSelector];
|
|
1151
|
+
schema.functionSignature = functionSignature;
|
|
1152
|
+
schema.functionSelector = functionSelector;
|
|
1153
|
+
schema.operationType = derivedOperationType;
|
|
1154
|
+
schema.operationName = operationName;
|
|
1155
|
+
schema.supportedActionsBitmap = supportedActionsBitmap;
|
|
1156
|
+
schema.isProtected = isProtected;
|
|
1157
|
+
schema.handlerForSelectors = handlerForSelectors;
|
|
1158
|
+
|
|
1159
|
+
// Add to supportedFunctionsSet
|
|
1160
|
+
if (!self.supportedFunctionsSet.add(bytes32(functionSelector))) {
|
|
1161
|
+
revert SharedValidation.OperationFailed();
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* @dev Removes a function schema from the system.
|
|
1167
|
+
* @param self The SecureOperationState to modify.
|
|
1168
|
+
* @param functionSelector The function selector to remove.
|
|
1169
|
+
* @param safeRemoval If true, reverts with ResourceAlreadyExists when any role still references this function.
|
|
1170
|
+
* The safeRemoval check is done inside this function (iterating supportedRolesSet directly) for efficiency.
|
|
1171
|
+
* @notice Security: Cannot remove protected function schemas to maintain system integrity.
|
|
1172
|
+
* @notice Cleanup: Automatically removes unused operation types from supportedOperationTypesSet.
|
|
1173
|
+
*/
|
|
1174
|
+
function removeFunctionSchema(
|
|
1175
|
+
SecureOperationState storage self,
|
|
1176
|
+
bytes4 functionSelector,
|
|
1177
|
+
bool safeRemoval
|
|
1178
|
+
) public {
|
|
1179
|
+
// Security check: Prevent removing protected function schemas
|
|
1180
|
+
// MUST check BEFORE removing from set to avoid inconsistent state
|
|
1181
|
+
if (self.functions[functionSelector].isProtected) {
|
|
1182
|
+
revert SharedValidation.CannotModifyProtected(bytes32(functionSelector));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// If safeRemoval: ensure no role references this function. Iterate supportedRolesSet directly for efficiency.
|
|
1186
|
+
if (safeRemoval) {
|
|
1187
|
+
uint256 rolesLength = self.supportedRolesSet.length();
|
|
1188
|
+
for (uint256 i = 0; i < rolesLength; i++) {
|
|
1189
|
+
bytes32 roleHash = self.supportedRolesSet.at(i);
|
|
1190
|
+
if (self.roles[roleHash].functionSelectorsSet.contains(bytes32(functionSelector))) {
|
|
1191
|
+
revert SharedValidation.ResourceAlreadyExists(bytes32(functionSelector));
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Store operation type before deletion (needed for cleanup check)
|
|
1197
|
+
bytes32 operationType = self.functions[functionSelector].operationType;
|
|
1198
|
+
|
|
1199
|
+
// Clear the function schema data
|
|
1200
|
+
// Remove the function schema from the supported functions set (O(1) operation)
|
|
1201
|
+
// MUST remove BEFORE checking if operation type is still in use, otherwise
|
|
1202
|
+
// _getFunctionsByOperationType will still find this function selector
|
|
1203
|
+
delete self.functions[functionSelector];
|
|
1204
|
+
if (!self.supportedFunctionsSet.remove(bytes32(functionSelector))) {
|
|
1205
|
+
revert SharedValidation.ResourceNotFound(bytes32(functionSelector));
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Check if the operation type is still in use by other functions.
|
|
1209
|
+
// Now that the function has been removed, this will correctly detect if the
|
|
1210
|
+
// operation type is no longer in use.
|
|
1211
|
+
bytes4[] memory functionsUsingOperationType = _getFunctionsByOperationType(self, operationType);
|
|
1212
|
+
if (functionsUsingOperationType.length == 0) {
|
|
1213
|
+
// Remove the operation type from supported operation types set if no longer in use
|
|
1214
|
+
if (!self.supportedOperationTypesSet.remove(operationType)) {
|
|
1215
|
+
// This should never happen, but defensive check for safety
|
|
1216
|
+
revert SharedValidation.OperationFailed();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* @dev Checks if a specific action is supported by a function.
|
|
1223
|
+
* @param self The SecureOperationState to check.
|
|
1224
|
+
* @param functionSelector The function selector to check.
|
|
1225
|
+
* @param action The action to check for support.
|
|
1226
|
+
* @return True if the action is supported by the function, false otherwise.
|
|
1227
|
+
*/
|
|
1228
|
+
function isActionSupportedByFunction(
|
|
1229
|
+
SecureOperationState storage self,
|
|
1230
|
+
bytes4 functionSelector,
|
|
1231
|
+
TxAction action
|
|
1232
|
+
) public view returns (bool) {
|
|
1233
|
+
// Check if function exists in supportedFunctionsSet
|
|
1234
|
+
if (!self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
FunctionSchema memory functionSchema = self.functions[functionSelector];
|
|
1239
|
+
return hasActionInBitmap(functionSchema.supportedActionsBitmap, action);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* @dev Adds a target address to the whitelist for a function selector.
|
|
1244
|
+
* @param self The SecureOperationState to modify.
|
|
1245
|
+
* @param functionSelector The function selector whose whitelist will be updated.
|
|
1246
|
+
* @param target The target address to add to the whitelist.
|
|
1247
|
+
*/
|
|
1248
|
+
function addTargetToFunctionWhitelist(
|
|
1249
|
+
SecureOperationState storage self,
|
|
1250
|
+
bytes4 functionSelector,
|
|
1251
|
+
address target
|
|
1252
|
+
) public {
|
|
1253
|
+
SharedValidation.validateNotZeroAddress(target);
|
|
1254
|
+
|
|
1255
|
+
// Function selector must be registered in the schema set
|
|
1256
|
+
if (!self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
|
|
1257
|
+
revert SharedValidation.ResourceNotFound(bytes32(functionSelector));
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
EnumerableSet.AddressSet storage set = self.functionTargetWhitelist[functionSelector];
|
|
1261
|
+
if (!set.add(target)) {
|
|
1262
|
+
revert SharedValidation.ItemAlreadyExists(target);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* @dev Removes a target address from the whitelist for a function selector.
|
|
1268
|
+
* @param self The SecureOperationState to modify.
|
|
1269
|
+
* @param functionSelector The function selector whose whitelist will be updated.
|
|
1270
|
+
* @param target The target address to remove from the whitelist.
|
|
1271
|
+
*/
|
|
1272
|
+
function removeTargetFromFunctionWhitelist(
|
|
1273
|
+
SecureOperationState storage self,
|
|
1274
|
+
bytes4 functionSelector,
|
|
1275
|
+
address target
|
|
1276
|
+
) public {
|
|
1277
|
+
EnumerableSet.AddressSet storage set = self.functionTargetWhitelist[functionSelector];
|
|
1278
|
+
if (!set.remove(target)) {
|
|
1279
|
+
revert SharedValidation.ItemNotFound(target);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* @dev Validates that the target address is whitelisted for the given function selector.
|
|
1285
|
+
* Internal contract calls (address(this)) are always allowed.
|
|
1286
|
+
* @param self The SecureOperationState to check.
|
|
1287
|
+
* @param functionSelector The function selector being executed.
|
|
1288
|
+
* @param target The target contract address.
|
|
1289
|
+
* @notice Target MUST be present in functionTargetWhitelist[functionSelector] unless target is address(this).
|
|
1290
|
+
* If whitelist is empty (no entries), no targets are allowed - explicit deny for security.
|
|
1291
|
+
*/
|
|
1292
|
+
function _validateFunctionTargetWhitelist(
|
|
1293
|
+
SecureOperationState storage self,
|
|
1294
|
+
bytes4 functionSelector,
|
|
1295
|
+
address target
|
|
1296
|
+
) internal view {
|
|
1297
|
+
// Fast path: selector not registered, skip validation
|
|
1298
|
+
if (!self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// SECURITY: Internal contract calls are always allowed
|
|
1303
|
+
// This enables internal execution functions to work without whitelist configuration
|
|
1304
|
+
if (target == address(this)) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
EnumerableSet.AddressSet storage set = self.functionTargetWhitelist[functionSelector];
|
|
1309
|
+
|
|
1310
|
+
// If target is in whitelist, validation passes
|
|
1311
|
+
if (set.contains(target)) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Target is not whitelisted for this function selector.
|
|
1316
|
+
revert SharedValidation.TargetNotWhitelisted(target, functionSelector);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* @dev Returns all whitelisted target addresses for a function selector.
|
|
1321
|
+
* @param self The SecureOperationState to check.
|
|
1322
|
+
* @param functionSelector The function selector to query.
|
|
1323
|
+
* @return Array of whitelisted target addresses.
|
|
1324
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1325
|
+
*/
|
|
1326
|
+
function getFunctionWhitelistTargets(
|
|
1327
|
+
SecureOperationState storage self,
|
|
1328
|
+
bytes4 functionSelector
|
|
1329
|
+
) public view returns (address[] memory) {
|
|
1330
|
+
EnumerableSet.AddressSet storage set = self.functionTargetWhitelist[functionSelector];
|
|
1331
|
+
return _convertAddressSetToArray(set);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// ============ FUNCTION TARGET HOOKS MANAGEMENT ============
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* @dev Adds a target address to the hooks for a function selector.
|
|
1338
|
+
* @param self The SecureOperationState to modify.
|
|
1339
|
+
* @param functionSelector The function selector whose hooks will be updated.
|
|
1340
|
+
* @param target The target address to add to the hooks.
|
|
1341
|
+
*/
|
|
1342
|
+
function addTargetToFunctionHooks(
|
|
1343
|
+
SecureOperationState storage self,
|
|
1344
|
+
bytes4 functionSelector,
|
|
1345
|
+
address target
|
|
1346
|
+
) public {
|
|
1347
|
+
SharedValidation.validateNotZeroAddress(target);
|
|
1348
|
+
|
|
1349
|
+
// Function selector must be registered in the schema set
|
|
1350
|
+
if (!self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
|
|
1351
|
+
revert SharedValidation.ResourceNotFound(bytes32(functionSelector));
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
EnumerableSet.AddressSet storage set = self.functionTargetHooks[functionSelector];
|
|
1355
|
+
|
|
1356
|
+
// Validate hook count limit
|
|
1357
|
+
SharedValidation.validateHookCount(
|
|
1358
|
+
set.length(),
|
|
1359
|
+
MAX_HOOKS_PER_SELECTOR
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
if (!set.add(target)) {
|
|
1363
|
+
revert SharedValidation.ItemAlreadyExists(target);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* @dev Removes a target address from the hooks for a function selector.
|
|
1369
|
+
* @param self The SecureOperationState to modify.
|
|
1370
|
+
* @param functionSelector The function selector whose hooks will be updated.
|
|
1371
|
+
* @param target The target address to remove from the hooks.
|
|
1372
|
+
*/
|
|
1373
|
+
function removeTargetFromFunctionHooks(
|
|
1374
|
+
SecureOperationState storage self,
|
|
1375
|
+
bytes4 functionSelector,
|
|
1376
|
+
address target
|
|
1377
|
+
) public {
|
|
1378
|
+
EnumerableSet.AddressSet storage set = self.functionTargetHooks[functionSelector];
|
|
1379
|
+
if (!set.remove(target)) {
|
|
1380
|
+
revert SharedValidation.ItemNotFound(target);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* @dev Returns all hook target addresses for a function selector.
|
|
1386
|
+
* @param self The SecureOperationState to check.
|
|
1387
|
+
* @param functionSelector The function selector to query.
|
|
1388
|
+
* @return Array of hook target addresses.
|
|
1389
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1390
|
+
*/
|
|
1391
|
+
function getFunctionHookTargets(
|
|
1392
|
+
SecureOperationState storage self,
|
|
1393
|
+
bytes4 functionSelector
|
|
1394
|
+
) public view returns (address[] memory) {
|
|
1395
|
+
EnumerableSet.AddressSet storage set = self.functionTargetHooks[functionSelector];
|
|
1396
|
+
return _convertAddressSetToArray(set);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* @dev Returns all function schemas that use a specific operation type.
|
|
1401
|
+
* @param self The SecureOperationState to check.
|
|
1402
|
+
* @param operationType The operation type to search for.
|
|
1403
|
+
* @return Array of function selectors that use the specified operation type.
|
|
1404
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1405
|
+
*/
|
|
1406
|
+
function getFunctionsByOperationType(
|
|
1407
|
+
SecureOperationState storage self,
|
|
1408
|
+
bytes32 operationType
|
|
1409
|
+
) public view returns (bytes4[] memory) {
|
|
1410
|
+
return _getFunctionsByOperationType(self, operationType);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* @dev Internal: Returns all function schemas that use a specific operation type.
|
|
1415
|
+
* Used by removeFunctionSchema and getFunctionsByOperationType.
|
|
1416
|
+
*/
|
|
1417
|
+
function _getFunctionsByOperationType(
|
|
1418
|
+
SecureOperationState storage self,
|
|
1419
|
+
bytes32 operationType
|
|
1420
|
+
) internal view returns (bytes4[] memory) {
|
|
1421
|
+
uint256 functionsLength = self.supportedFunctionsSet.length();
|
|
1422
|
+
bytes4[] memory tempResults = new bytes4[](functionsLength);
|
|
1423
|
+
uint256 resultCount = 0;
|
|
1424
|
+
|
|
1425
|
+
for (uint i = 0; i < functionsLength; i++) {
|
|
1426
|
+
bytes4 functionSelector = bytes4(self.supportedFunctionsSet.at(i));
|
|
1427
|
+
FunctionSchema memory functionSchema = self.functions[functionSelector];
|
|
1428
|
+
if (functionSchema.operationType == operationType) {
|
|
1429
|
+
tempResults[resultCount] = functionSelector;
|
|
1430
|
+
resultCount++;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
bytes4[] memory result = new bytes4[](resultCount);
|
|
1435
|
+
for (uint i = 0; i < resultCount; i++) {
|
|
1436
|
+
result[i] = tempResults[i];
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
return result;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
// ============ BACKWARD COMPATIBILITY FUNCTIONS ============
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* @dev Gets all pending transaction IDs as an array for backward compatibility
|
|
1447
|
+
* @param self The SecureOperationState to check
|
|
1448
|
+
* @return Array of pending transaction IDs
|
|
1449
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1450
|
+
*/
|
|
1451
|
+
function getPendingTransactionsList(SecureOperationState storage self) public view returns (uint256[] memory) {
|
|
1452
|
+
return _convertUintSetToArray(self.pendingTransactionsSet);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* @dev Gets all supported roles as an array for backward compatibility
|
|
1457
|
+
* @param self The SecureOperationState to check
|
|
1458
|
+
* @return Array of supported role hashes
|
|
1459
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1460
|
+
*/
|
|
1461
|
+
function getSupportedRolesList(SecureOperationState storage self) public view returns (bytes32[] memory) {
|
|
1462
|
+
return _convertBytes32SetToArray(self.supportedRolesSet);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* @dev Gets all supported function selectors as an array for backward compatibility
|
|
1467
|
+
* @param self The SecureOperationState to check
|
|
1468
|
+
* @return Array of supported function selectors
|
|
1469
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1470
|
+
*/
|
|
1471
|
+
function getSupportedFunctionsList(SecureOperationState storage self) public view returns (bytes4[] memory) {
|
|
1472
|
+
return _convertBytes4SetToArray(self.supportedFunctionsSet);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* @dev Gets all supported operation types as an array for backward compatibility
|
|
1477
|
+
* @param self The SecureOperationState to check
|
|
1478
|
+
* @return Array of supported operation type hashes
|
|
1479
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1480
|
+
*/
|
|
1481
|
+
function getSupportedOperationTypesList(SecureOperationState storage self) public view returns (bytes32[] memory) {
|
|
1482
|
+
return _convertBytes32SetToArray(self.supportedOperationTypesSet);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* @dev Gets the authorized wallet at a specific index from a role
|
|
1487
|
+
* @param self The SecureOperationState to check
|
|
1488
|
+
* @param roleHash The role hash to get the wallet from
|
|
1489
|
+
* @param index The index position of the wallet to retrieve
|
|
1490
|
+
* @return The authorized wallet address at the specified index
|
|
1491
|
+
*/
|
|
1492
|
+
function getAuthorizedWalletAt(SecureOperationState storage self, bytes32 roleHash, uint256 index) public view returns (address) {
|
|
1493
|
+
Role storage role = self.roles[roleHash];
|
|
1494
|
+
SharedValidation.validateIndexInBounds(index, role.authorizedWallets.length());
|
|
1495
|
+
return role.authorizedWallets.at(index);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* @dev Gets all function permissions for a role as an array for backward compatibility
|
|
1500
|
+
* @param self The SecureOperationState to check
|
|
1501
|
+
* @param roleHash The role hash to get function permissions from
|
|
1502
|
+
* @return Array of function permissions with arrays (for external API)
|
|
1503
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1504
|
+
*/
|
|
1505
|
+
function getRoleFunctionPermissions(SecureOperationState storage self, bytes32 roleHash) public view returns (FunctionPermission[] memory) {
|
|
1506
|
+
Role storage role = self.roles[roleHash];
|
|
1507
|
+
|
|
1508
|
+
uint256 length = role.functionSelectorsSet.length();
|
|
1509
|
+
FunctionPermission[] memory result = new FunctionPermission[](length);
|
|
1510
|
+
|
|
1511
|
+
for (uint256 i = 0; i < length; i++) {
|
|
1512
|
+
bytes4 functionSelector = bytes4(role.functionSelectorsSet.at(i));
|
|
1513
|
+
result[i] = role.functionPermissions[functionSelector];
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
return result;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* @dev Gets all roles assigned to a wallet using the reverse index
|
|
1521
|
+
* @param self The SecureOperationState to check
|
|
1522
|
+
* @param wallet The wallet address to get roles for
|
|
1523
|
+
* @return Array of role hashes assigned to the wallet
|
|
1524
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1525
|
+
* @notice This function uses the reverse index (walletRoles) for efficient O(n) lookup where n = wallet's role count
|
|
1526
|
+
*/
|
|
1527
|
+
function getWalletRoles(SecureOperationState storage self, address wallet) public view returns (bytes32[] memory) {
|
|
1528
|
+
EnumerableSet.Bytes32Set storage walletRolesSet = self.walletRoles[wallet];
|
|
1529
|
+
return _convertBytes32SetToArray(walletRolesSet);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// ============ META-TRANSACTION SUPPORT FUNCTIONS ============
|
|
1533
|
+
|
|
1534
|
+
/**
|
|
1535
|
+
* @dev Gets the current nonce for a specific signer.
|
|
1536
|
+
* @param self The SecureOperationState to check.
|
|
1537
|
+
* @param signer The address of the signer.
|
|
1538
|
+
* @return The current nonce for the signer.
|
|
1539
|
+
* @notice Access control should be enforced by the calling contract.
|
|
1540
|
+
*/
|
|
1541
|
+
function getSignerNonce(SecureOperationState storage self, address signer) public view returns (uint256) {
|
|
1542
|
+
return self.signerNonces[signer];
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* @dev Increments the nonce for a specific signer.
|
|
1547
|
+
* @param self The SecureOperationState to modify.
|
|
1548
|
+
* @param signer The address of the signer.
|
|
1549
|
+
*/
|
|
1550
|
+
function incrementSignerNonce(SecureOperationState storage self, address signer) private {
|
|
1551
|
+
self.signerNonces[signer]++;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* @dev Verifies the signature of a meta-transaction with detailed error reporting
|
|
1556
|
+
* @param self The SecureOperationState to check against
|
|
1557
|
+
* @param metaTx The meta-transaction containing the signature to verify
|
|
1558
|
+
* @return True if the signature is valid, false otherwise
|
|
1559
|
+
*/
|
|
1560
|
+
function verifySignature(
|
|
1561
|
+
SecureOperationState storage self,
|
|
1562
|
+
MetaTransaction memory metaTx
|
|
1563
|
+
) private view returns (bool) {
|
|
1564
|
+
// Basic validation
|
|
1565
|
+
SharedValidation.validateSignatureLength(metaTx.signature);
|
|
1566
|
+
_validateTxStatus(self, metaTx.txRecord.txId, TxStatus.PENDING);
|
|
1567
|
+
|
|
1568
|
+
// Transaction parameters validation
|
|
1569
|
+
SharedValidation.validateNotZeroAddress(metaTx.txRecord.params.requester);
|
|
1570
|
+
|
|
1571
|
+
// Meta-transaction parameters validation
|
|
1572
|
+
SharedValidation.validateChainId(metaTx.params.chainId);
|
|
1573
|
+
SharedValidation.validateMetaTxDeadline(metaTx.params.deadline);
|
|
1574
|
+
|
|
1575
|
+
// Gas price validation (if applicable)
|
|
1576
|
+
SharedValidation.validateGasPrice(metaTx.params.maxGasPrice);
|
|
1577
|
+
|
|
1578
|
+
// Validate signer-specific nonce
|
|
1579
|
+
SharedValidation.validateNonce(metaTx.params.nonce, getSignerNonce(self, metaTx.params.signer));
|
|
1580
|
+
|
|
1581
|
+
// txId validation for new meta transactions
|
|
1582
|
+
if (metaTx.params.action == TxAction.SIGN_META_REQUEST_AND_APPROVE) {
|
|
1583
|
+
SharedValidation.validateTransactionId(metaTx.txRecord.txId, self.txCounter);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Authorization check - verify signer has meta-transaction signing permissions for the function and action
|
|
1587
|
+
bool isSignAction = metaTx.params.action == TxAction.SIGN_META_REQUEST_AND_APPROVE || metaTx.params.action == TxAction.SIGN_META_APPROVE || metaTx.params.action == TxAction.SIGN_META_CANCEL;
|
|
1588
|
+
bool isHandlerAuthorized = hasActionPermission(self, metaTx.params.signer, metaTx.params.handlerSelector, metaTx.params.action);
|
|
1589
|
+
bool isExecutionAuthorized = hasActionPermission(self, metaTx.params.signer, metaTx.txRecord.params.executionSelector, metaTx.params.action);
|
|
1590
|
+
if (!isSignAction || !isHandlerAuthorized || !isExecutionAuthorized) {
|
|
1591
|
+
revert SharedValidation.SignerNotAuthorized(metaTx.params.signer);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// Signature verification
|
|
1595
|
+
bytes32 messageHash = generateMessageHash(metaTx);
|
|
1596
|
+
address recoveredSigner = recoverSigner(messageHash, metaTx.signature);
|
|
1597
|
+
if (recoveredSigner != metaTx.params.signer) revert SharedValidation.InvalidSignature(metaTx.signature);
|
|
1598
|
+
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* @dev Generates a message hash for the specified meta-transaction following EIP-712
|
|
1604
|
+
* @param metaTx The meta-transaction to generate the hash for
|
|
1605
|
+
* @return The generated message hash
|
|
1606
|
+
*/
|
|
1607
|
+
function generateMessageHash(MetaTransaction memory metaTx) private view returns (bytes32) {
|
|
1608
|
+
bytes32 domainSeparator = keccak256(abi.encode(
|
|
1609
|
+
DOMAIN_SEPARATOR_TYPE_HASH,
|
|
1610
|
+
PROTOCOL_NAME_HASH,
|
|
1611
|
+
keccak256(abi.encodePacked(VERSION_MAJOR, ".", VERSION_MINOR, ".", VERSION_PATCH)),
|
|
1612
|
+
block.chainid,
|
|
1613
|
+
address(this)
|
|
1614
|
+
));
|
|
1615
|
+
|
|
1616
|
+
bytes32 structHash = keccak256(abi.encode(
|
|
1617
|
+
TYPE_HASH,
|
|
1618
|
+
keccak256(abi.encode(
|
|
1619
|
+
metaTx.txRecord.txId,
|
|
1620
|
+
metaTx.txRecord.params.requester,
|
|
1621
|
+
metaTx.txRecord.params.target,
|
|
1622
|
+
metaTx.txRecord.params.value,
|
|
1623
|
+
metaTx.txRecord.params.gasLimit,
|
|
1624
|
+
metaTx.txRecord.params.operationType,
|
|
1625
|
+
metaTx.txRecord.params.executionSelector,
|
|
1626
|
+
keccak256(metaTx.txRecord.params.executionParams)
|
|
1627
|
+
)),
|
|
1628
|
+
metaTx.params.chainId,
|
|
1629
|
+
metaTx.params.nonce,
|
|
1630
|
+
metaTx.params.handlerContract,
|
|
1631
|
+
metaTx.params.handlerSelector,
|
|
1632
|
+
uint8(metaTx.params.action),
|
|
1633
|
+
metaTx.params.deadline,
|
|
1634
|
+
metaTx.params.maxGasPrice,
|
|
1635
|
+
metaTx.params.signer
|
|
1636
|
+
));
|
|
1637
|
+
|
|
1638
|
+
return keccak256(abi.encodePacked(
|
|
1639
|
+
"\x19\x01",
|
|
1640
|
+
domainSeparator,
|
|
1641
|
+
structHash
|
|
1642
|
+
));
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* @dev Recovers the signer address from a message hash and signature.
|
|
1647
|
+
* @param messageHash The hash of the message that was signed.
|
|
1648
|
+
* @param signature The signature to recover the address from.
|
|
1649
|
+
* @return The address of the signer.
|
|
1650
|
+
*/
|
|
1651
|
+
function recoverSigner(bytes32 messageHash, bytes memory signature) public pure returns (address) {
|
|
1652
|
+
SharedValidation.validateSignatureLength(signature);
|
|
1653
|
+
|
|
1654
|
+
bytes32 r;
|
|
1655
|
+
bytes32 s;
|
|
1656
|
+
uint8 v;
|
|
1657
|
+
|
|
1658
|
+
// More efficient assembly block with better memory safety
|
|
1659
|
+
assembly {
|
|
1660
|
+
// First 32 bytes stores the length of the signature
|
|
1661
|
+
// add(signature, 32) = pointer of sig + 32
|
|
1662
|
+
// effectively, skips first 32 bytes of signature
|
|
1663
|
+
r := mload(add(signature, 0x20))
|
|
1664
|
+
// add(signature, 64) = pointer of sig + 64
|
|
1665
|
+
// effectively, skips first 64 bytes of signature
|
|
1666
|
+
s := mload(add(signature, 0x40))
|
|
1667
|
+
// add(signature, 96) = pointer of sig + 96
|
|
1668
|
+
// effectively, skips first 96 bytes of signature
|
|
1669
|
+
// byte(0, mload(add(signature, 96))) = first byte of the next 32 bytes
|
|
1670
|
+
v := byte(0, mload(add(signature, 0x60)))
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
|
|
1674
|
+
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
|
|
1675
|
+
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}
|
|
1676
|
+
SharedValidation.validateSignatureParams(s, v);
|
|
1677
|
+
|
|
1678
|
+
address signer = ecrecover(messageHash.toEthSignedMessageHash(), v, r, s);
|
|
1679
|
+
SharedValidation.validateRecoveredSigner(signer);
|
|
1680
|
+
|
|
1681
|
+
return signer;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
/**
|
|
1686
|
+
* @dev Creates a meta-transaction for a new operation
|
|
1687
|
+
*/
|
|
1688
|
+
function generateUnsignedForNewMetaTx(
|
|
1689
|
+
SecureOperationState storage self,
|
|
1690
|
+
TxParams memory txParams,
|
|
1691
|
+
MetaTxParams memory metaTxParams
|
|
1692
|
+
) public view returns (MetaTransaction memory) {
|
|
1693
|
+
SharedValidation.validateNotZeroAddress(txParams.target);
|
|
1694
|
+
|
|
1695
|
+
TxRecord memory txRecord = createNewTxRecord(
|
|
1696
|
+
self,
|
|
1697
|
+
txParams.requester,
|
|
1698
|
+
txParams.target,
|
|
1699
|
+
txParams.value,
|
|
1700
|
+
txParams.gasLimit,
|
|
1701
|
+
txParams.operationType,
|
|
1702
|
+
txParams.executionSelector,
|
|
1703
|
+
txParams.executionParams
|
|
1704
|
+
);
|
|
1705
|
+
|
|
1706
|
+
return generateMetaTransaction(self, txRecord, metaTxParams);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* @dev Creates a meta-transaction for an existing transaction
|
|
1711
|
+
*/
|
|
1712
|
+
function generateUnsignedForExistingMetaTx(
|
|
1713
|
+
SecureOperationState storage self,
|
|
1714
|
+
uint256 txId,
|
|
1715
|
+
MetaTxParams memory metaTxParams
|
|
1716
|
+
) public view returns (MetaTransaction memory) {
|
|
1717
|
+
TxRecord memory txRecord = getTxRecord(self, txId);
|
|
1718
|
+
if (txRecord.txId != txId) revert SharedValidation.ResourceNotFound(bytes32(uint256(txId)));
|
|
1719
|
+
|
|
1720
|
+
return generateMetaTransaction(self, txRecord, metaTxParams);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* @notice Creates a meta-transaction structure with populated nonce from storage
|
|
1725
|
+
* @dev Initializes a MetaTransaction with transaction record data and empty signature fields.
|
|
1726
|
+
* The nonce is populated directly from storage for security. The caller is responsible
|
|
1727
|
+
* for filling in the following fields:
|
|
1728
|
+
* - handlerContract: The contract that will handle the meta-transaction
|
|
1729
|
+
* - handlerSelector: The function selector for the handler
|
|
1730
|
+
* - deadline: The timestamp after which the meta-transaction expires
|
|
1731
|
+
* - maxGasPrice: The maximum gas price allowed for execution
|
|
1732
|
+
* - signer: The address that will sign the meta-transaction
|
|
1733
|
+
* @param self The SecureOperationState to reference for nonce
|
|
1734
|
+
* @param txRecord The transaction record to include in the meta-transaction
|
|
1735
|
+
* @param metaTxParams The meta-transaction parameters to include in the meta-transaction
|
|
1736
|
+
* @return MetaTransaction A new meta-transaction structure with default values
|
|
1737
|
+
*/
|
|
1738
|
+
function generateMetaTransaction(
|
|
1739
|
+
SecureOperationState storage self,
|
|
1740
|
+
TxRecord memory txRecord,
|
|
1741
|
+
MetaTxParams memory metaTxParams
|
|
1742
|
+
) private view returns (MetaTransaction memory) {
|
|
1743
|
+
SharedValidation.validateChainId(metaTxParams.chainId);
|
|
1744
|
+
SharedValidation.validateHandlerContract(metaTxParams.handlerContract);
|
|
1745
|
+
SharedValidation.validateHandlerSelector(metaTxParams.handlerSelector);
|
|
1746
|
+
SharedValidation.validateDeadline(metaTxParams.deadline);
|
|
1747
|
+
SharedValidation.validateNotZeroAddress(metaTxParams.signer);
|
|
1748
|
+
|
|
1749
|
+
// Populate the nonce directly from storage for security
|
|
1750
|
+
metaTxParams.nonce = getSignerNonce(self, metaTxParams.signer);
|
|
1751
|
+
|
|
1752
|
+
MetaTransaction memory metaTx = MetaTransaction({
|
|
1753
|
+
txRecord: txRecord,
|
|
1754
|
+
params: metaTxParams,
|
|
1755
|
+
message: 0,
|
|
1756
|
+
signature: "",
|
|
1757
|
+
data: prepareTransactionData(txRecord)
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
// Generate the message hash for ready to sign meta-transaction
|
|
1761
|
+
bytes32 msgHash = generateMessageHash(metaTx);
|
|
1762
|
+
metaTx.message = msgHash;
|
|
1763
|
+
|
|
1764
|
+
return metaTx;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* @notice Creates meta-transaction parameters with specified values
|
|
1769
|
+
* @dev Helper function to create properly formatted MetaTxParams
|
|
1770
|
+
* @param handlerContract The contract that will handle the meta-transaction
|
|
1771
|
+
* @param handlerSelector The function selector for the handler
|
|
1772
|
+
* @param action The transaction action type
|
|
1773
|
+
* @param deadline The timestamp after which the meta-transaction expires
|
|
1774
|
+
* @param maxGasPrice The maximum gas price allowed for execution
|
|
1775
|
+
* @param signer The address that will sign the meta-transaction
|
|
1776
|
+
* @return MetaTxParams The formatted meta-transaction parameters
|
|
1777
|
+
*/
|
|
1778
|
+
function createMetaTxParams(
|
|
1779
|
+
address handlerContract,
|
|
1780
|
+
bytes4 handlerSelector,
|
|
1781
|
+
TxAction action,
|
|
1782
|
+
uint256 deadline,
|
|
1783
|
+
uint256 maxGasPrice,
|
|
1784
|
+
address signer
|
|
1785
|
+
) public view returns (MetaTxParams memory) {
|
|
1786
|
+
SharedValidation.validateHandlerContract(handlerContract);
|
|
1787
|
+
SharedValidation.validateHandlerSelector(handlerSelector);
|
|
1788
|
+
SharedValidation.validateNotZeroAddress(signer);
|
|
1789
|
+
return MetaTxParams({
|
|
1790
|
+
chainId: block.chainid,
|
|
1791
|
+
nonce: 0, // Uninitialized - will be populated in generateMetaTransaction
|
|
1792
|
+
handlerContract: handlerContract,
|
|
1793
|
+
handlerSelector: handlerSelector,
|
|
1794
|
+
action: action,
|
|
1795
|
+
deadline: block.timestamp + deadline * 1 seconds,
|
|
1796
|
+
maxGasPrice: maxGasPrice,
|
|
1797
|
+
signer: signer
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// ============ EVENT FUNCTIONS ============
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* @dev Logs an event by emitting TransactionEvent and forwarding to event forwarder
|
|
1805
|
+
* @param self The SecureOperationState
|
|
1806
|
+
* @param txId The transaction ID
|
|
1807
|
+
* @param functionSelector The function selector to emit in the event
|
|
1808
|
+
* @custom:security REENTRANCY PROTECTION: This function is safe from reentrancy because:
|
|
1809
|
+
* 1. It is called AFTER all state changes are complete (in _completeTransaction,
|
|
1810
|
+
* _cancelTransaction, and txRequest)
|
|
1811
|
+
* 2. It only reads state and emits events - no critical state modifications
|
|
1812
|
+
* 3. The external call to eventForwarder is wrapped in try-catch, so failures
|
|
1813
|
+
* don't affect contract state
|
|
1814
|
+
* 4. Even if eventForwarder is malicious and tries to reenter, all entry functions
|
|
1815
|
+
* require PENDING status, but transactions are already in COMPLETED/CANCELLED
|
|
1816
|
+
* status at this point, preventing reentry
|
|
1817
|
+
* This is a false positive from static analysis - the function is reentrancy-safe.
|
|
1818
|
+
*/
|
|
1819
|
+
function logTxEvent(
|
|
1820
|
+
SecureOperationState storage self,
|
|
1821
|
+
uint256 txId,
|
|
1822
|
+
bytes4 functionSelector
|
|
1823
|
+
) public {
|
|
1824
|
+
TxRecord memory txRecord = self.txRecords[txId];
|
|
1825
|
+
|
|
1826
|
+
// Emit only non-sensitive public data
|
|
1827
|
+
emit TransactionEvent(
|
|
1828
|
+
txId,
|
|
1829
|
+
functionSelector,
|
|
1830
|
+
txRecord.status,
|
|
1831
|
+
txRecord.params.requester,
|
|
1832
|
+
txRecord.params.target,
|
|
1833
|
+
txRecord.params.operationType
|
|
1834
|
+
);
|
|
1835
|
+
|
|
1836
|
+
// Forward event data to event forwarder
|
|
1837
|
+
// REENTRANCY SAFE: External call is wrapped in try-catch and doesn't modify
|
|
1838
|
+
// critical state. Even if eventForwarder is malicious, reentry attempts fail
|
|
1839
|
+
// because transactions are no longer in PENDING status (they're COMPLETED/CANCELLED).
|
|
1840
|
+
if (self.eventForwarder != address(0)) {
|
|
1841
|
+
try IEventForwarder(self.eventForwarder).forwardTxEvent(
|
|
1842
|
+
txId,
|
|
1843
|
+
functionSelector,
|
|
1844
|
+
txRecord.status,
|
|
1845
|
+
txRecord.params.requester,
|
|
1846
|
+
txRecord.params.target,
|
|
1847
|
+
txRecord.params.operationType
|
|
1848
|
+
) {
|
|
1849
|
+
// Event forwarded successfully
|
|
1850
|
+
} catch {
|
|
1851
|
+
// Forwarding failed, continue execution (non-critical operation)
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* @dev Set the event forwarder for this specific instance
|
|
1858
|
+
* @param self The SecureOperationState
|
|
1859
|
+
* @param forwarder The event forwarder address
|
|
1860
|
+
*/
|
|
1861
|
+
function setEventForwarder(
|
|
1862
|
+
SecureOperationState storage self,
|
|
1863
|
+
address forwarder
|
|
1864
|
+
) public {
|
|
1865
|
+
self.eventForwarder = forwarder;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// ============ BITMAP HELPER FUNCTIONS ============
|
|
1869
|
+
|
|
1870
|
+
/**
|
|
1871
|
+
* @dev Checks if a TxAction is present in a bitmap
|
|
1872
|
+
* @param bitmap The bitmap to check
|
|
1873
|
+
* @param action The TxAction to check for
|
|
1874
|
+
* @return True if the action is present in the bitmap
|
|
1875
|
+
*/
|
|
1876
|
+
function hasActionInBitmap(uint16 bitmap, TxAction action) internal pure returns (bool) {
|
|
1877
|
+
return (bitmap & (1 << uint8(action))) != 0;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/**
|
|
1881
|
+
* @dev Adds a TxAction to a bitmap
|
|
1882
|
+
* @param bitmap The original bitmap
|
|
1883
|
+
* @param action The TxAction to add
|
|
1884
|
+
* @return The updated bitmap with the action added
|
|
1885
|
+
*/
|
|
1886
|
+
function addActionToBitmap(uint16 bitmap, TxAction action) internal pure returns (uint16) {
|
|
1887
|
+
return uint16(bitmap | (1 << uint8(action)));
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/**
|
|
1891
|
+
* @dev Creates a bitmap from an array of TxActions
|
|
1892
|
+
* @param actions Array of TxActions to convert to bitmap
|
|
1893
|
+
* @return Bitmap representation of the actions
|
|
1894
|
+
*/
|
|
1895
|
+
function createBitmapFromActions(TxAction[] memory actions) internal pure returns (uint16) {
|
|
1896
|
+
uint16 bitmap = 0;
|
|
1897
|
+
for (uint i = 0; i < actions.length; i++) {
|
|
1898
|
+
bitmap = addActionToBitmap(bitmap, actions[i]);
|
|
1899
|
+
}
|
|
1900
|
+
return bitmap;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* @dev Converts a bitmap to an array of TxActions
|
|
1905
|
+
* @param bitmap The bitmap to convert
|
|
1906
|
+
* @return Array of TxActions represented by the bitmap
|
|
1907
|
+
*/
|
|
1908
|
+
function convertBitmapToActions(uint16 bitmap) internal pure returns (TxAction[] memory) {
|
|
1909
|
+
// Count how many actions are set
|
|
1910
|
+
uint256 count = 0;
|
|
1911
|
+
for (uint8 i = 0; i < 16; i++) {
|
|
1912
|
+
if ((bitmap & (1 << i)) != 0) {
|
|
1913
|
+
count++;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Create array and populate it
|
|
1918
|
+
TxAction[] memory actions = new TxAction[](count);
|
|
1919
|
+
uint256 index = 0;
|
|
1920
|
+
for (uint8 i = 0; i < 16; i++) {
|
|
1921
|
+
if ((bitmap & (1 << i)) != 0) {
|
|
1922
|
+
actions[index] = TxAction(i);
|
|
1923
|
+
index++;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
return actions;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
// ============ OPTIMIZATION HELPER FUNCTIONS ============
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* @dev Helper function to complete a transaction and remove from pending list
|
|
1935
|
+
* @param self The SecureOperationState to modify
|
|
1936
|
+
* @param txId The transaction ID to complete
|
|
1937
|
+
* @param success Whether the transaction execution was successful
|
|
1938
|
+
* @param result The result of the transaction execution
|
|
1939
|
+
*/
|
|
1940
|
+
function _completeTransaction(
|
|
1941
|
+
SecureOperationState storage self,
|
|
1942
|
+
uint256 txId,
|
|
1943
|
+
bool success,
|
|
1944
|
+
bytes memory result
|
|
1945
|
+
) private {
|
|
1946
|
+
// enforce that the requested target is whitelisted for this selector.
|
|
1947
|
+
_validateFunctionTargetWhitelist(self, self.txRecords[txId].params.executionSelector, self.txRecords[txId].params.target);
|
|
1948
|
+
|
|
1949
|
+
// Update storage with new status and result
|
|
1950
|
+
if (success) {
|
|
1951
|
+
self.txRecords[txId].status = TxStatus.COMPLETED;
|
|
1952
|
+
self.txRecords[txId].result = result;
|
|
1953
|
+
} else {
|
|
1954
|
+
self.txRecords[txId].status = TxStatus.FAILED;
|
|
1955
|
+
self.txRecords[txId].result = result; // Store failure reason for debugging
|
|
1956
|
+
// Note: FAILED status is intentional - transactions can be valid when requested
|
|
1957
|
+
// but fail when executed (e.g., conditions changed, insufficient balance, etc.)
|
|
1958
|
+
// Users can query status via getTransaction() or listen to TransactionEvent
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// Remove from pending transactions list
|
|
1962
|
+
removeFromPendingTransactionsList(self, txId);
|
|
1963
|
+
|
|
1964
|
+
logTxEvent(self, txId, self.txRecords[txId].params.executionSelector);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* @dev Helper function to cancel a transaction and remove from pending list
|
|
1969
|
+
* @param self The SecureOperationState to modify
|
|
1970
|
+
* @param txId The transaction ID to cancel
|
|
1971
|
+
*/
|
|
1972
|
+
function _cancelTransaction(
|
|
1973
|
+
SecureOperationState storage self,
|
|
1974
|
+
uint256 txId
|
|
1975
|
+
) private {
|
|
1976
|
+
// enforce that the requested target is whitelisted for this selector.
|
|
1977
|
+
_validateFunctionTargetWhitelist(self, self.txRecords[txId].params.executionSelector, self.txRecords[txId].params.target);
|
|
1978
|
+
|
|
1979
|
+
self.txRecords[txId].status = TxStatus.CANCELLED;
|
|
1980
|
+
|
|
1981
|
+
// Remove from pending transactions list
|
|
1982
|
+
removeFromPendingTransactionsList(self, txId);
|
|
1983
|
+
|
|
1984
|
+
logTxEvent(self, txId, self.txRecords[txId].params.executionSelector);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/**
|
|
1988
|
+
* @dev Validates that the caller has any role permission
|
|
1989
|
+
* @param self The SecureOperationState to check
|
|
1990
|
+
* @notice This function consolidates the repeated permission check pattern to reduce contract size
|
|
1991
|
+
*/
|
|
1992
|
+
function _validateAnyRole(SecureOperationState storage self) internal view {
|
|
1993
|
+
if (!hasAnyRole(self, msg.sender)) revert SharedValidation.NoPermission(msg.sender);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
/**
|
|
1997
|
+
* @dev Validates that a role exists by checking if its hash is not zero
|
|
1998
|
+
* @param self The SecureOperationState to check
|
|
1999
|
+
* @param roleHash The role hash to validate
|
|
2000
|
+
* @notice This function consolidates the repeated role existence check pattern to reduce contract size
|
|
2001
|
+
*/
|
|
2002
|
+
function _validateRoleExists(SecureOperationState storage self, bytes32 roleHash) internal view {
|
|
2003
|
+
if (self.roles[roleHash].roleHash == 0 || !self.supportedRolesSet.contains(roleHash)) {
|
|
2004
|
+
revert SharedValidation.ResourceNotFound(roleHash);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* @dev Validates that a transaction is in the expected status
|
|
2010
|
+
* @param self The SecureOperationState to check
|
|
2011
|
+
* @param txId The transaction ID to validate
|
|
2012
|
+
* @param expectedStatus The expected transaction status
|
|
2013
|
+
* @notice This function consolidates the repeated transaction status check pattern to reduce contract size.
|
|
2014
|
+
* REENTRANCY PROTECTION: This validation is a critical part of the state machine reentrancy guard:
|
|
2015
|
+
* 1. Entry functions set status to EXECUTING before calling executeTransaction
|
|
2016
|
+
* (following Checks-Effects-Interactions pattern)
|
|
2017
|
+
* 2. If reentry is attempted, the transaction status is EXECUTING (not PENDING)
|
|
2018
|
+
* 3. All entry functions check for PENDING status first via _validateTxStatus(..., PENDING)
|
|
2019
|
+
* 4. Reentry attempts fail because status check fails (EXECUTING != PENDING)
|
|
2020
|
+
* This creates a one-way state machine: PENDING → EXECUTING → (COMPLETED/FAILED)
|
|
2021
|
+
* that prevents reentrancy without additional storage overhead.
|
|
2022
|
+
*/
|
|
2023
|
+
function _validateTxStatus(
|
|
2024
|
+
SecureOperationState storage self,
|
|
2025
|
+
uint256 txId,
|
|
2026
|
+
TxStatus expectedStatus
|
|
2027
|
+
) internal view {
|
|
2028
|
+
TxStatus currentStatus = self.txRecords[txId].status;
|
|
2029
|
+
if (currentStatus != expectedStatus) {
|
|
2030
|
+
revert SharedValidation.TransactionStatusMismatch(uint8(expectedStatus), uint8(currentStatus));
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* @dev Validates that a wallet has permission for both execution selector and handler selector for a given action
|
|
2036
|
+
* @param self The SecureOperationState to check
|
|
2037
|
+
* @param wallet The wallet address to check permissions for
|
|
2038
|
+
* @param executionSelector The execution function selector (underlying operation)
|
|
2039
|
+
* @param handlerSelector The handler/calling function selector
|
|
2040
|
+
* @param action The action to validate permissions for
|
|
2041
|
+
* @notice This function consolidates the repeated dual permission check pattern to reduce contract size
|
|
2042
|
+
* @notice Reverts with NoPermission if either permission check fails
|
|
2043
|
+
*/
|
|
2044
|
+
function _validateExecutionAndHandlerPermissions(
|
|
2045
|
+
SecureOperationState storage self,
|
|
2046
|
+
address wallet,
|
|
2047
|
+
bytes4 executionSelector,
|
|
2048
|
+
bytes4 handlerSelector,
|
|
2049
|
+
TxAction action
|
|
2050
|
+
) internal view {
|
|
2051
|
+
// Validate permission for the execution selector (underlying operation)
|
|
2052
|
+
if (!hasActionPermission(self, wallet, executionSelector, action)) {
|
|
2053
|
+
revert SharedValidation.NoPermission(wallet);
|
|
2054
|
+
}
|
|
2055
|
+
// Validate permission for the handler/calling function selector (e.g. msg.sig)
|
|
2056
|
+
if (!hasActionPermission(self, wallet, handlerSelector, action)) {
|
|
2057
|
+
revert SharedValidation.NoPermission(wallet);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
/**
|
|
2062
|
+
* @dev Validates that all handlerForSelectors are present in the schema's handlerForSelectors array
|
|
2063
|
+
* @param self The SecureOperationState to validate against
|
|
2064
|
+
* @param functionSelector The function selector for which the permission is defined
|
|
2065
|
+
* @param handlerForSelectors The handlerForSelectors array from the permission to validate
|
|
2066
|
+
* @notice Reverts with HandlerForSelectorMismatch if any handlerForSelector is not found in the schema's array
|
|
2067
|
+
* @notice Special case: Execution function permissions should include functionSelector in handlerForSelectors (self-reference)
|
|
2068
|
+
*/
|
|
2069
|
+
function _validateHandlerForSelectors(
|
|
2070
|
+
SecureOperationState storage self,
|
|
2071
|
+
bytes4 functionSelector,
|
|
2072
|
+
bytes4[] memory handlerForSelectors
|
|
2073
|
+
) internal view {
|
|
2074
|
+
bytes32 functionSelectorHash = bytes32(functionSelector);
|
|
2075
|
+
|
|
2076
|
+
// Ensure the function schema exists
|
|
2077
|
+
if (!self.supportedFunctionsSet.contains(functionSelectorHash)) {
|
|
2078
|
+
revert SharedValidation.ResourceNotFound(functionSelectorHash);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
FunctionSchema storage schema = self.functions[functionSelector];
|
|
2082
|
+
|
|
2083
|
+
// Validate each handlerForSelector in the array
|
|
2084
|
+
for (uint256 j = 0; j < handlerForSelectors.length; j++) {
|
|
2085
|
+
bytes4 handlerForSelector = handlerForSelectors[j];
|
|
2086
|
+
|
|
2087
|
+
// Special case: execution function permissions use handlerForSelector == functionSelector (self-reference)
|
|
2088
|
+
if (handlerForSelector == functionSelector) {
|
|
2089
|
+
continue; // Valid execution function permission
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
bool found = false;
|
|
2093
|
+
for (uint256 i = 0; i < schema.handlerForSelectors.length; i++) {
|
|
2094
|
+
if (schema.handlerForSelectors[i] == handlerForSelector) {
|
|
2095
|
+
found = true;
|
|
2096
|
+
break;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (!found) {
|
|
2100
|
+
revert SharedValidation.HandlerForSelectorMismatch(
|
|
2101
|
+
bytes4(0), // Cannot return array, use 0 as placeholder
|
|
2102
|
+
handlerForSelector
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* @dev Validates meta-transaction permissions for a function permission
|
|
2110
|
+
* @param self The secure operation state
|
|
2111
|
+
* @param functionPermission The function permission to validate
|
|
2112
|
+
* @custom:security This function prevents conflicting meta-sign and meta-execute permissions
|
|
2113
|
+
*/
|
|
2114
|
+
function _validateMetaTxPermissions(
|
|
2115
|
+
SecureOperationState storage self,
|
|
2116
|
+
FunctionPermission memory functionPermission
|
|
2117
|
+
) internal view {
|
|
2118
|
+
uint16 bitmap = functionPermission.grantedActionsBitmap;
|
|
2119
|
+
|
|
2120
|
+
// Revert if permissions are empty (bitmap is 0) to prevent silent failures
|
|
2121
|
+
if (bitmap == 0) {
|
|
2122
|
+
revert SharedValidation.NotSupported();
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// Create bitmasks for meta-sign and meta-execute actions
|
|
2126
|
+
// Meta-sign actions: SIGN_META_REQUEST_AND_APPROVE (3), SIGN_META_APPROVE (4), SIGN_META_CANCEL (5)
|
|
2127
|
+
uint16 metaSignMask = (1 << 3) | (1 << 4) | (1 << 5);
|
|
2128
|
+
|
|
2129
|
+
// Meta-execute actions: EXECUTE_META_REQUEST_AND_APPROVE (6), EXECUTE_META_APPROVE (7), EXECUTE_META_CANCEL (8)
|
|
2130
|
+
uint16 metaExecuteMask = (1 << 6) | (1 << 7) | (1 << 8);
|
|
2131
|
+
|
|
2132
|
+
// Check if any meta-sign actions are present
|
|
2133
|
+
bool hasMetaSign = (bitmap & metaSignMask) != 0;
|
|
2134
|
+
|
|
2135
|
+
// Check if any meta-execute actions are present
|
|
2136
|
+
bool hasMetaExecute = (bitmap & metaExecuteMask) != 0;
|
|
2137
|
+
|
|
2138
|
+
// If both flags are raised, this is a security misconfiguration
|
|
2139
|
+
if (hasMetaSign && hasMetaExecute) {
|
|
2140
|
+
revert SharedValidation.ConflictingMetaTxPermissions(functionPermission.functionSelector);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// Validate that each action in the bitmap is supported by the function
|
|
2144
|
+
// This still requires iteration, but we can optimize it
|
|
2145
|
+
for (uint i = 0; i < 9; i++) { // TxAction enum has 9 values (0-8)
|
|
2146
|
+
if (hasActionInBitmap(bitmap, TxAction(i))) {
|
|
2147
|
+
if (!isActionSupportedByFunction(self, functionPermission.functionSelector, TxAction(i))) {
|
|
2148
|
+
revert SharedValidation.NotSupported();
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/**
|
|
2155
|
+
* @dev Generic helper to convert AddressSet to array
|
|
2156
|
+
* @param set The EnumerableSet.AddressSet to convert
|
|
2157
|
+
* @return Array of address values
|
|
2158
|
+
*/
|
|
2159
|
+
function _convertAddressSetToArray(EnumerableSet.AddressSet storage set)
|
|
2160
|
+
internal view returns (address[] memory) {
|
|
2161
|
+
uint256 length = set.length();
|
|
2162
|
+
address[] memory result = new address[](length);
|
|
2163
|
+
for (uint256 i = 0; i < length; i++) {
|
|
2164
|
+
result[i] = set.at(i);
|
|
2165
|
+
}
|
|
2166
|
+
return result;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/**
|
|
2170
|
+
* @dev Generic helper to convert UintSet to array
|
|
2171
|
+
* @param set The EnumerableSet.UintSet to convert
|
|
2172
|
+
* @return Array of uint256 values
|
|
2173
|
+
*/
|
|
2174
|
+
function _convertUintSetToArray(EnumerableSet.UintSet storage set)
|
|
2175
|
+
internal view returns (uint256[] memory) {
|
|
2176
|
+
uint256 length = set.length();
|
|
2177
|
+
uint256[] memory result = new uint256[](length);
|
|
2178
|
+
for (uint256 i = 0; i < length; i++) {
|
|
2179
|
+
result[i] = set.at(i);
|
|
2180
|
+
}
|
|
2181
|
+
return result;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/**
|
|
2185
|
+
* @dev Generic helper to convert Bytes32Set to array
|
|
2186
|
+
* @param set The EnumerableSet.Bytes32Set to convert
|
|
2187
|
+
* @return Array of bytes32 values
|
|
2188
|
+
*/
|
|
2189
|
+
function _convertBytes32SetToArray(EnumerableSet.Bytes32Set storage set)
|
|
2190
|
+
internal view returns (bytes32[] memory) {
|
|
2191
|
+
uint256 length = set.length();
|
|
2192
|
+
bytes32[] memory result = new bytes32[](length);
|
|
2193
|
+
for (uint256 i = 0; i < length; i++) {
|
|
2194
|
+
result[i] = set.at(i);
|
|
2195
|
+
}
|
|
2196
|
+
return result;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* @dev Generic helper to convert Bytes32Set (containing bytes4 selectors) to bytes4 array
|
|
2201
|
+
* @param set The EnumerableSet.Bytes32Set to convert (stores bytes4 selectors as bytes32)
|
|
2202
|
+
* @return Array of bytes4 function selectors
|
|
2203
|
+
*/
|
|
2204
|
+
function _convertBytes4SetToArray(EnumerableSet.Bytes32Set storage set)
|
|
2205
|
+
internal view returns (bytes4[] memory) {
|
|
2206
|
+
uint256 length = set.length();
|
|
2207
|
+
bytes4[] memory result = new bytes4[](length);
|
|
2208
|
+
for (uint256 i = 0; i < length; i++) {
|
|
2209
|
+
result[i] = bytes4(set.at(i));
|
|
2210
|
+
}
|
|
2211
|
+
return result;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* @dev Validates that if a function exists in contract bytecode, it must be protected
|
|
2216
|
+
* @param functionSignature The function signature to check
|
|
2217
|
+
* @param functionSelector The function selector
|
|
2218
|
+
* @param isProtected Whether the function is marked as protected
|
|
2219
|
+
* @notice Checks if the function selector exists in the contract's bytecode function selector table
|
|
2220
|
+
* @notice If the selector exists in the contract, it must be protected to prevent accidental removal
|
|
2221
|
+
* @notice This uses low-level bytecode inspection instead of relying on naming conventions
|
|
2222
|
+
* @notice Since we're called via delegatecall, address(this) refers to the calling contract
|
|
2223
|
+
*/
|
|
2224
|
+
function _validateContractFunctionProtection(
|
|
2225
|
+
string memory functionSignature,
|
|
2226
|
+
bytes4 functionSelector,
|
|
2227
|
+
bool isProtected
|
|
2228
|
+
) private view {
|
|
2229
|
+
// Check if the function selector exists in the contract's bytecode
|
|
2230
|
+
// Since we're called via delegatecall, address(this) refers to the calling contract
|
|
2231
|
+
if (selectorExistsInContract(address(this), functionSelector)) {
|
|
2232
|
+
if (!isProtected) {
|
|
2233
|
+
revert SharedValidation.ContractFunctionMustBeProtected(functionSelector, functionSignature);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* @dev Checks if a function selector exists in a contract's bytecode
|
|
2240
|
+
* @param contractAddress The address of the contract to check
|
|
2241
|
+
* @param selector The 4-byte function selector to search for
|
|
2242
|
+
* @return true if the selector is found in the contract's function selector dispatch table area
|
|
2243
|
+
* @notice Searches the first 2KB where function selectors are stored in the dispatch table
|
|
2244
|
+
* @notice This is a heuristic check - false positives are possible but unlikely
|
|
2245
|
+
* @notice Uses loop unrolling for gas efficiency
|
|
2246
|
+
* @notice Can be used to query any contract's function selector table
|
|
2247
|
+
*/
|
|
2248
|
+
function selectorExistsInContract(address contractAddress, bytes4 selector) public view returns (bool) {
|
|
2249
|
+
// Get the contract's bytecode
|
|
2250
|
+
bytes memory code = contractAddress.code;
|
|
2251
|
+
|
|
2252
|
+
if (code.length < 5) { // Need at least PUSH4 (1 byte) + selector (4 bytes)
|
|
2253
|
+
return false;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Function selectors are in the dispatch table at the beginning
|
|
2257
|
+
// Typical dispatch tables are < 2KB even for large contracts
|
|
2258
|
+
// Searching only this area reduces gas cost and false positives from metadata/data
|
|
2259
|
+
uint256 searchLength = code.length < 2048 ? code.length : 2048;
|
|
2260
|
+
|
|
2261
|
+
// Scan for PUSH4 opcode (0x63) with 1-byte sliding window
|
|
2262
|
+
// PUSH4 opcode is followed by 4 bytes which is the function selector
|
|
2263
|
+
// Function selectors in EVM bytecode are emitted as PUSH4 <selector> instructions
|
|
2264
|
+
// These can start at any byte offset, not just 4-byte-aligned positions
|
|
2265
|
+
for (uint256 i = 0; i + 4 < searchLength; i++) {
|
|
2266
|
+
// Check if current byte is PUSH4 opcode (0x63)
|
|
2267
|
+
if (uint8(code[i]) == 0x63) {
|
|
2268
|
+
// Extract the 4-byte selector following the PUSH4 opcode
|
|
2269
|
+
bytes4 candidate;
|
|
2270
|
+
assembly {
|
|
2271
|
+
let codePtr := add(add(code, 0x20), add(i, 1))
|
|
2272
|
+
candidate := mload(codePtr)
|
|
2273
|
+
}
|
|
2274
|
+
if (candidate == selector) {
|
|
2275
|
+
return true;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
}
|