@bloxchain/contracts 1.0.0-alpha

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