@bloxchain/contracts 1.0.0-alpha.13 → 1.0.0-alpha.14

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