@bloxchain/contracts 1.0.0-alpha.7 → 1.0.0

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 (52) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +8 -9
  3. package/abi/BaseStateMachine.abi.json +773 -822
  4. package/abi/EngineBlox.abi.json +562 -552
  5. package/abi/GuardController.abi.json +1597 -1609
  6. package/abi/GuardControllerDefinitions.abi.json +235 -199
  7. package/abi/IDefinition.abi.json +57 -47
  8. package/abi/RuntimeRBAC.abi.json +841 -842
  9. package/abi/RuntimeRBACDefinitions.abi.json +212 -202
  10. package/abi/SecureOwnable.abi.json +1365 -1349
  11. package/abi/SecureOwnableDefinitions.abi.json +174 -164
  12. package/core/AUDIT.md +45 -0
  13. package/core/access/RuntimeRBAC.sol +130 -61
  14. package/core/access/interface/IRuntimeRBAC.sol +3 -3
  15. package/core/access/lib/definitions/RuntimeRBACDefinitions.sol +7 -3
  16. package/core/base/BaseStateMachine.sol +971 -967
  17. package/core/base/interface/IBaseStateMachine.sol +153 -160
  18. package/core/execution/GuardController.sol +89 -75
  19. package/core/execution/interface/IGuardController.sol +146 -160
  20. package/core/execution/lib/definitions/GuardControllerDefinitions.sol +136 -25
  21. package/core/lib/EngineBlox.sol +577 -327
  22. package/core/lib/interfaces/IDefinition.sol +49 -49
  23. package/core/lib/interfaces/IEventForwarder.sol +4 -2
  24. package/core/lib/utils/SharedValidation.sol +534 -490
  25. package/core/pattern/Account.sol +84 -75
  26. package/core/security/SecureOwnable.sol +446 -390
  27. package/core/security/interface/ISecureOwnable.sol +105 -105
  28. package/core/security/lib/definitions/SecureOwnableDefinitions.sol +49 -17
  29. package/package.json +51 -49
  30. package/standards/behavior/ICopyable.sol +3 -11
  31. package/standards/hooks/IOnActionHook.sol +1 -1
  32. package/abi/AccountBlox.abi.json +0 -3935
  33. package/abi/BareBlox.abi.json +0 -1378
  34. package/abi/RoleBlox.abi.json +0 -2983
  35. package/abi/SecureBlox.abi.json +0 -2753
  36. package/abi/SimpleRWA20.abi.json +0 -4032
  37. package/abi/SimpleRWA20Definitions.abi.json +0 -191
  38. package/abi/SimpleVault.abi.json +0 -3407
  39. package/abi/SimpleVaultDefinitions.abi.json +0 -269
  40. package/core/research/BloxchainWallet.sol +0 -133
  41. package/core/research/FactoryBlox/FactoryBlox.sol +0 -343
  42. package/core/research/FactoryBlox/FactoryBloxDefinitions.sol +0 -143
  43. package/core/research/erc1155-blox/ERC1155Blox.sol +0 -169
  44. package/core/research/erc1155-blox/lib/definitions/ERC1155BloxDefinitions.sol +0 -203
  45. package/core/research/erc20-blox/ERC20Blox.sol +0 -167
  46. package/core/research/erc20-blox/lib/definitions/ERC20BloxDefinitions.sol +0 -185
  47. package/core/research/erc721-blox/ERC721Blox.sol +0 -131
  48. package/core/research/erc721-blox/lib/definitions/ERC721BloxDefinitions.sol +0 -172
  49. package/core/research/lending-blox/.gitkeep +0 -1
  50. package/core/research/p2p-blox/P2PBlox.sol +0 -266
  51. package/core/research/p2p-blox/README.md +0 -85
  52. package/core/research/p2p-blox/lib/definitions/P2PBloxDefinitions.sol +0 -19
@@ -1,9 +1,8 @@
1
1
  // SPDX-License-Identifier: MPL-2.0
2
- pragma solidity 0.8.33;
2
+ pragma solidity 0.8.35;
3
3
 
4
4
  import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
5
  import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
- import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
7
6
  import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
8
7
 
9
8
  // Local imports
@@ -29,13 +28,18 @@ import "./interfaces/IEventForwarder.sol";
29
28
  *
30
29
  * The library is designed to be used as a building block for secure smart contract systems
31
30
  * that require high levels of security and flexibility through state abstraction.
31
+ *
32
+ * @notice Enumeration vs execution gas: Whitelist enforcement (`_validateTargetWhitelist`) uses set membership
33
+ * (`contains`) and is O(1) in whitelist length. Helpers that return full sets (`getSupportedRoles`,
34
+ * `getPendingTransactions`, `getFunctionWhitelistTargets`, `_convert*SetToArray`, etc.) allocate and iterate
35
+ * in O(set size)—primarily view/RPC or admin cleanup cost, not unbounded execution scans of those sets.
36
+ * Immutable `MAX_ROLES`, `MAX_FUNCTIONS`, `MAX_HOOKS_PER_SELECTOR`, and `MAX_BATCH_SIZE` cap their domains;
37
+ * per-role `maxWallets` is operator-chosen (only required > 0) and scales `removeRole` gas linearly.
32
38
  */
33
39
  library EngineBlox {
34
40
  // ============ VERSION INFORMATION ============
35
41
  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;
42
+ string public constant VERSION = "1.0.0";
39
43
 
40
44
  // ============ SYSTEM SAFETY LIMITS ============
41
45
  // These constants define the safety range limits for system operations
@@ -53,7 +57,6 @@ library EngineBlox {
53
57
  /// @dev Maximum total number of functions allowed in the system (prevents gas exhaustion in function operations)
54
58
  uint256 public constant MAX_FUNCTIONS = 2000;
55
59
 
56
- using MessageHashUtils for bytes32;
57
60
  using SharedValidation for *;
58
61
  using EnumerableSet for EnumerableSet.UintSet;
59
62
  using EnumerableSet for EnumerableSet.Bytes32Set;
@@ -67,8 +70,7 @@ library EngineBlox {
67
70
  PROCESSING_PAYMENT,
68
71
  CANCELLED,
69
72
  COMPLETED,
70
- FAILED,
71
- REJECTED
73
+ FAILED
72
74
  }
73
75
 
74
76
  enum TxAction {
@@ -110,7 +112,9 @@ library EngineBlox {
110
112
  TxStatus status;
111
113
  TxParams params;
112
114
  bytes32 message;
113
- bytes result;
115
+ /// @dev Commitment to execution returndata: `bytes32(0)` when empty, else `keccak256(returndata)`.
116
+ /// Full returndata is emitted in `TxExecutionResult` on terminal execution (COMPLETED/FAILED).
117
+ bytes32 resultHash;
114
118
  PaymentDetails payment;
115
119
  }
116
120
 
@@ -152,8 +156,14 @@ library EngineBlox {
152
156
  bytes32 operationType;
153
157
  string operationName;
154
158
  uint16 supportedActionsBitmap; // Bitmap for TxAction enum (9 bits max)
159
+ /// @dev When true (strict mode): handlerForSelectors in role permissions must match this schema's handlerForSelectors at use time.
160
+ /// When false (flexible mode): no such check; forward references and unregistered selectors in handlerForSelectors are allowed at registration.
161
+ bool enforceHandlerRelations;
155
162
  bool isProtected;
156
- bytes4[] handlerForSelectors;
163
+ /// @dev When false, `removeFunctionFromRole` cannot remove this selector from any role (revoke + re-add updates blocked).
164
+ /// When true, grants may be removed from any role, including protected system roles; `isProtected` still blocks `unregisterFunction` for the schema.
165
+ bool isGrantRevocable;
166
+ bytes4[] handlerForSelectors;
157
167
  }
158
168
 
159
169
  // ============ DEFINITION STRUCTS ============
@@ -202,10 +212,38 @@ library EngineBlox {
202
212
 
203
213
  // Native token transfer selector (reserved signature unlikely to exist in real contracts)
204
214
  bytes4 public constant NATIVE_TRANSFER_SELECTOR = bytes4(keccak256("__bloxchain_native_transfer__()"));
205
- bytes32 public constant NATIVE_TRANSFER_OPERATION = keccak256("NATIVE_TRANSFER");
215
+
216
+ /// @dev Reserved pseudo-selector: whitelist key for `PaymentDetails.recipient` (native and ERC20 attached payments).
217
+ /// Function schema is registered by `GuardControllerDefinitions` (and tests/helpers that mirror it), not in `initialize`.
218
+ bytes4 public constant ATTACHED_PAYMENT_RECIPIENT_SELECTOR = bytes4(keccak256("__bloxchain_attached_payment_recipient__()"));
219
+
220
+ /// @dev Standard IERC20 `transfer(address,uint256)`; whitelist key for token contracts used in attached ERC20 payments.
221
+ /// Function schema is registered by `GuardControllerDefinitions` (and tests/helpers that mirror it), not in `initialize`.
222
+ bytes4 public constant ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)"));
206
223
 
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)");
224
+ // EIP-712 Type Hashes (selective meta-tx payload: MetaTxRecord = txId + params + payment only)
225
+ // These follow the canonical EIP-712 convention so that eth_signTypedData_v4 and equivalent
226
+ // wallet typed-data signers can reproduce the same hashes when given matching type definitions.
227
+ //
228
+ // Canonical primary type string for MetaTransaction (primary type + all referenced types,
229
+ // appended in alphabetical order by type name
230
+ bytes32 private constant META_TX_TYPE_HASH = keccak256(
231
+ "MetaTransaction(MetaTxRecord txRecord,MetaTxParams params,bytes data)"
232
+ "MetaTxParams(uint256 chainId,uint256 nonce,address handlerContract,bytes4 handlerSelector,uint8 action,uint256 deadline,uint256 maxGasPrice,address signer)"
233
+ "MetaTxRecord(uint256 txId,TxParams params,PaymentDetails payment)"
234
+ "PaymentDetails(address recipient,uint256 nativeTokenAmount,address erc20TokenAddress,uint256 erc20TokenAmount)"
235
+ "TxParams(address requester,address target,uint256 value,uint256 gasLimit,bytes32 operationType,bytes4 executionSelector,bytes executionParams)"
236
+ );
237
+
238
+ // Canonical primary type string for MetaTxRecord (primary type + its referenced types).
239
+ bytes32 private constant META_TX_RECORD_TYPE_HASH = keccak256(
240
+ "MetaTxRecord(uint256 txId,TxParams params,PaymentDetails payment)"
241
+ "PaymentDetails(address recipient,uint256 nativeTokenAmount,address erc20TokenAddress,uint256 erc20TokenAmount)"
242
+ "TxParams(address requester,address target,uint256 value,uint256 gasLimit,bytes32 operationType,bytes4 executionSelector,bytes executionParams)"
243
+ );
244
+ bytes32 private constant TX_PARAMS_TYPE_HASH = keccak256("TxParams(address requester,address target,uint256 value,uint256 gasLimit,bytes32 operationType,bytes4 executionSelector,bytes executionParams)");
245
+ bytes32 private constant META_TX_PARAMS_TYPE_HASH = keccak256("MetaTxParams(uint256 chainId,uint256 nonce,address handlerContract,bytes4 handlerSelector,uint8 action,uint256 deadline,uint256 maxGasPrice,address signer)");
246
+ bytes32 private constant PAYMENT_DETAILS_TYPE_HASH = keccak256("PaymentDetails(address recipient,uint256 nativeTokenAmount,address erc20TokenAddress,uint256 erc20TokenAmount)");
209
247
  bytes32 private constant DOMAIN_SEPARATOR_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
210
248
 
211
249
 
@@ -215,9 +253,15 @@ library EngineBlox {
215
253
  TxStatus status,
216
254
  address indexed requester,
217
255
  address target,
218
- bytes32 operationType
256
+ bytes32 operationType,
257
+ bytes32 resultHash
219
258
  );
220
259
 
260
+ /// @dev Emitted only on terminal execution (COMPLETED/FAILED). Full execution returndata (`result` may be empty).
261
+ /// Verify against `TxRecord.resultHash` from the same tx: `result.length == 0` implies `resultHash == bytes32(0)`;
262
+ /// otherwise `keccak256(result) == resultHash` (see `_executionResultHash`).
263
+ event TxExecutionResult(uint256 indexed txId, bytes result);
264
+
221
265
  // ============ SYSTEM STATE FUNCTIONS ============
222
266
 
223
267
  /**
@@ -266,11 +310,11 @@ library EngineBlox {
266
310
  /**
267
311
  * @dev Updates the time lock period for the SecureOperationState.
268
312
  * @param self The SecureOperationState to modify.
269
- * @param _newTimeLockPeriodSec The new time lock period in seconds.
313
+ * @param periodSec The new time lock period in seconds.
270
314
  */
271
- function updateTimeLockPeriod(SecureOperationState storage self, uint256 _newTimeLockPeriodSec) public {
272
- SharedValidation.validateTimeLockPeriod(_newTimeLockPeriodSec);
273
- self.timeLockPeriodSec = _newTimeLockPeriodSec;
315
+ function updateTimeLockPeriod(SecureOperationState storage self, uint256 periodSec) public {
316
+ SharedValidation.validateTimeLockPeriod(periodSec);
317
+ self.timeLockPeriodSec = periodSec;
274
318
  }
275
319
 
276
320
  // ============ TRANSACTION MANAGEMENT FUNCTIONS ============
@@ -291,12 +335,12 @@ library EngineBlox {
291
335
  * @param self The SecureOperationState to modify.
292
336
  * @param requester The address of the requester.
293
337
  * @param target The target contract address for the transaction.
294
- * @param value The value to send with the transaction.
338
+ * @param value The value to send with the transaction (typically 0 for standard calls; non-zero allowed when intentionally forwarding native ETH).
295
339
  * @param gasLimit The gas limit for the transaction.
296
340
  * @param operationType The type of operation.
297
341
  * @param handlerSelector The function selector of the handler/request function.
298
- * @param executionSelector The function selector to execute (NATIVE_TRANSFER_SELECTOR for simple native token transfers).
299
- * @param executionParams The encoded parameters for the function (empty for simple native token transfers).
342
+ * @param executionSelector The function selector to execute (NATIVE_TRANSFER_SELECTOR is a convenience selector for native-only transfers).
343
+ * @param executionParams The encoded parameters for the function (must be empty when using NATIVE_TRANSFER_SELECTOR).
300
344
  * @return The created TxRecord.
301
345
  */
302
346
  function txRequest(
@@ -322,7 +366,7 @@ library EngineBlox {
322
366
  operationType,
323
367
  executionSelector,
324
368
  executionParams,
325
- _noPayment()
369
+ _emptyPayment()
326
370
  );
327
371
  }
328
372
 
@@ -331,7 +375,7 @@ library EngineBlox {
331
375
  * @param self The SecureOperationState to modify.
332
376
  * @param requester The address of the requester.
333
377
  * @param target The target contract address for the transaction.
334
- * @param value The value to send with the transaction.
378
+ * @param value The value to send with the transaction (typically 0 for standard calls; non-zero allowed when intentionally forwarding native ETH).
335
379
  * @param gasLimit The gas limit for the transaction.
336
380
  * @param operationType The type of operation.
337
381
  * @param handlerSelector The function selector of the handler/request function.
@@ -356,6 +400,8 @@ library EngineBlox {
356
400
  // Validate both execution and handler selector permissions (same as txRequest)
357
401
  _validateExecutionAndHandlerPermissions(self, msg.sender, executionSelector, handlerSelector, TxAction.EXECUTE_TIME_DELAY_REQUEST);
358
402
 
403
+ _validateAttachedPaymentPolicy(self, paymentDetails);
404
+
359
405
  return _txRequest(
360
406
  self,
361
407
  requester,
@@ -394,11 +440,10 @@ library EngineBlox {
394
440
  bytes memory executionParams,
395
441
  PaymentDetails memory paymentDetails
396
442
  ) private returns (TxRecord memory) {
397
- SharedValidation.validateNotZeroAddress(target);
398
- // enforce that the requested target is whitelisted for this selector.
399
- _validateFunctionTargetWhitelist(self, executionSelector, target);
443
+ // Target non-zero and whitelist enforced in `_validateTargetWhitelist`.
444
+ _validateTargetWhitelist(self, executionSelector, target);
400
445
 
401
- TxRecord memory txRequestRecord = createNewTxRecord(
446
+ TxRecord memory txRequestRecord = createTxRecord(
402
447
  self,
403
448
  requester,
404
449
  target,
@@ -414,7 +459,7 @@ library EngineBlox {
414
459
  self.txCounter++;
415
460
 
416
461
  // Add to pending transactions list
417
- addToPendingTransactionsList(self, txRequestRecord.txId);
462
+ addPendingTx(self, txRequestRecord.txId);
418
463
 
419
464
  logTxEvent(self, txRequestRecord.txId, executionSelector);
420
465
 
@@ -433,14 +478,15 @@ library EngineBlox {
433
478
  uint256 txId,
434
479
  bytes4 handlerSelector
435
480
  ) public returns (TxRecord memory) {
436
- // Validate both execution and handler selector permissions
481
+ // CHECK: Validate both execution and handler selector permissions
437
482
  _validateExecutionAndHandlerPermissions(self, msg.sender, self.txRecords[txId].params.executionSelector, handlerSelector, TxAction.EXECUTE_TIME_DELAY_APPROVE);
438
483
  _validateTxStatus(self, txId, TxStatus.PENDING);
484
+ _validateTargetWhitelist(self, self.txRecords[txId].params.executionSelector, self.txRecords[txId].params.target);
439
485
  SharedValidation.validateReleaseTime(self.txRecords[txId].releaseTime);
440
-
486
+
441
487
  // EFFECT: Update status to EXECUTING before external call to prevent reentrancy
442
488
  self.txRecords[txId].status = TxStatus.EXECUTING;
443
-
489
+
444
490
  // INTERACT: External call after state update
445
491
  (bool success, bytes memory result) = executeTransaction(self, self.txRecords[txId]);
446
492
 
@@ -477,9 +523,13 @@ library EngineBlox {
477
523
  */
478
524
  function txCancellationWithMetaTx(SecureOperationState storage self, MetaTransaction memory metaTx) public returns (TxRecord memory) {
479
525
  uint256 txId = metaTx.txRecord.txId;
526
+ _validateMetaTxAction(metaTx, TxAction.SIGN_META_CANCEL);
527
+
480
528
  // Validate both execution and handler selector permissions
481
529
  _validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_CANCEL);
482
530
  _validateTxStatus(self, txId, TxStatus.PENDING);
531
+ _validateMetaTxMatchRecord(self, txId, metaTx.txRecord);
532
+ _validateMetaTxPaymentMatchRecord(self, txId, metaTx.txRecord);
483
533
  if (!verifySignature(self, metaTx)) revert SharedValidation.InvalidSignature(metaTx.signature);
484
534
 
485
535
  incrementSignerNonce(self, metaTx.params.signer);
@@ -495,6 +545,8 @@ library EngineBlox {
495
545
  * @return The updated TxRecord.
496
546
  */
497
547
  function txApprovalWithMetaTx(SecureOperationState storage self, MetaTransaction memory metaTx) public returns (TxRecord memory) {
548
+ _validateMetaTxAction(metaTx, TxAction.SIGN_META_APPROVE);
549
+
498
550
  // Validate both execution and handler selector permissions
499
551
  _validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_APPROVE);
500
552
 
@@ -508,17 +560,25 @@ library EngineBlox {
508
560
  * @return The updated TxRecord.
509
561
  * @notice This function skips permission validation and should only be called from functions
510
562
  * that have already validated permissions.
563
+ * @custom:security TIMELOCK: The releaseTime (timelock) is intentionally NOT enforced on this path.
564
+ * This is by design: the meta-tx workflow allows authorized signers to approve execution
565
+ * without waiting for releaseTime, providing a hybrid synergy between timelock workflows
566
+ * (direct path enforces releaseTime) and meta-tx workflows (delegated, time-flexible approval).
511
567
  */
512
568
  function _txApprovalWithMetaTx(SecureOperationState storage self, MetaTransaction memory metaTx) private returns (TxRecord memory) {
569
+ // CHECK: Validate transaction parameters
513
570
  uint256 txId = metaTx.txRecord.txId;
514
571
  _validateTxStatus(self, txId, TxStatus.PENDING);
572
+ _validateTargetWhitelist(self, self.txRecords[txId].params.executionSelector, self.txRecords[txId].params.target);
573
+ _validateMetaTxMatchRecord(self, txId, metaTx.txRecord);
574
+ _validateMetaTxPaymentMatchRecord(self, txId, metaTx.txRecord);
515
575
  if (!verifySignature(self, metaTx)) revert SharedValidation.InvalidSignature(metaTx.signature);
516
-
576
+
517
577
  incrementSignerNonce(self, metaTx.params.signer);
518
-
578
+
519
579
  // EFFECT: Update status to EXECUTING before external call to prevent reentrancy
520
580
  self.txRecords[txId].status = TxStatus.EXECUTING;
521
-
581
+
522
582
  // INTERACT: External call after state update
523
583
  (bool success, bytes memory result) = executeTransaction(self, self.txRecords[txId]);
524
584
 
@@ -537,9 +597,13 @@ library EngineBlox {
537
597
  SecureOperationState storage self,
538
598
  MetaTransaction memory metaTx
539
599
  ) public returns (TxRecord memory) {
600
+ _validateMetaTxAction(metaTx, TxAction.SIGN_META_REQUEST_AND_APPROVE);
601
+
540
602
  // Validate both execution and handler selector permissions
541
603
  _validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_REQUEST_AND_APPROVE);
542
-
604
+
605
+ _validateAttachedPaymentPolicy(self, metaTx.txRecord.payment);
606
+
543
607
  TxRecord memory txRecord = _txRequest(
544
608
  self,
545
609
  metaTx.txRecord.params.requester,
@@ -570,13 +634,21 @@ library EngineBlox {
570
634
  * causing _validateTxPending to revert in entry functions
571
635
  * 4. Status flow is one-way: PENDING → EXECUTING → (COMPLETED/FAILED)
572
636
  * This creates an effective reentrancy guard without additional storage overhead.
637
+ * @notice **Atomicity:** If the main call succeeds but `executeAttachedPayment` reverts (e.g.
638
+ * insufficient balance, whitelist mismatch), the **entire** approval/execute transaction reverts—main
639
+ * effect included. This is **intentional all-or-nothing** semantics; splitting finalize vs payment
640
+ * would require a separate design with reentrancy and state-machine implications.
641
+ * @notice `record` is a memory copy: final `status` and `resultHash` are written to storage by `_completeTransaction`;
642
+ * full returndata is emitted in `TxExecutionResult`.
573
643
  */
574
644
  function executeTransaction(SecureOperationState storage self, TxRecord memory record) private returns (bool, bytes memory) {
575
645
  // Validate that transaction is in EXECUTING status (set by caller before this function)
576
646
  // This proves reentrancy protection is active at entry point
577
647
  _validateTxStatus(self, record.txId, TxStatus.EXECUTING);
578
648
 
579
- bytes memory txData = prepareTransactionData(record);
649
+ bytes memory txData = buildCallData(record);
650
+ // gasLimit == 0 → forward all remaining gas (conventional "no cap"). Integrators that want
651
+ // a strict upper bound must set a positive gasLimit in TxParams at request time.
580
652
  uint gas = record.params.gasLimit;
581
653
  if (gas == 0) {
582
654
  gas = gasleft();
@@ -590,16 +662,10 @@ library EngineBlox {
590
662
  );
591
663
 
592
664
  if (success) {
593
- record.status = TxStatus.COMPLETED;
594
- record.result = result;
595
-
596
665
  // Execute attached payment if transaction was successful
597
666
  if (record.payment.recipient != address(0)) {
598
667
  executeAttachedPayment(self, record);
599
668
  }
600
- } else {
601
- record.status = TxStatus.FAILED;
602
- record.result = result;
603
669
  }
604
670
 
605
671
  return (success, result);
@@ -618,6 +684,13 @@ library EngineBlox {
618
684
  * 4. All entry functions check for PENDING status first, so reentry fails
619
685
  * The external calls (native token transfer, ERC20 transfer) cannot reenter
620
686
  * critical functions because the transaction is no longer in PENDING state.
687
+ * @notice When payment amounts are non-zero, `payment.recipient` is validated against
688
+ * `ATTACHED_PAYMENT_RECIPIENT_SELECTOR`; non-zero ERC20 amounts also require
689
+ * `payment.erc20TokenAddress` on `ERC20_TRANSFER_SELECTOR` whitelist.
690
+ * @notice **ERC20 attached payouts** use `safeTransfer` with the **nominal** `erc20TokenAmount`.
691
+ * **Fee-on-transfer, rebasing, or other non-standard ERC20s are not supported:** the protocol does not
692
+ * measure balance deltas at the recipient; operators must only attach **standard** tokens where
693
+ * transferred amount equals the requested amount.
621
694
  */
622
695
  function executeAttachedPayment(
623
696
  SecureOperationState storage self,
@@ -626,6 +699,9 @@ library EngineBlox {
626
699
  // Validate that transaction is still in EXECUTING status
627
700
  // This ensures reentrancy protection is maintained throughout payment execution
628
701
  _validateTxStatus(self, record.txId, TxStatus.EXECUTING);
702
+
703
+ _validateAttachedPaymentPolicy(self, record.payment);
704
+
629
705
  self.txRecords[record.txId].status = TxStatus.PROCESSING_PAYMENT;
630
706
 
631
707
  PaymentDetails memory payment = record.payment;
@@ -662,11 +738,11 @@ library EngineBlox {
662
738
  }
663
739
 
664
740
  /**
665
- * @dev Prepares transaction data from execution selector and params without executing it.
741
+ * @dev Builds transaction call data from execution selector and params without executing it.
666
742
  * @param record The transaction record to prepare data for.
667
743
  * @return The prepared transaction data.
668
744
  */
669
- function prepareTransactionData(TxRecord memory record) private pure returns (bytes memory) {
745
+ function buildCallData(TxRecord memory record) private pure returns (bytes memory) {
670
746
  // If executionSelector is NATIVE_TRANSFER_SELECTOR, it's a simple native token transfer (no function call)
671
747
  if (record.params.executionSelector == NATIVE_TRANSFER_SELECTOR) {
672
748
  // SECURITY: Validate empty params to prevent confusion with real function calls
@@ -683,7 +759,7 @@ library EngineBlox {
683
759
 
684
760
 
685
761
  /**
686
- * @notice Creates a new transaction record with basic fields populated
762
+ * @notice Creates a transaction record with basic fields populated
687
763
  * @dev Initializes a TxRecord struct with the provided parameters and default values
688
764
  * @param self The SecureOperationState to reference for txId and timelock
689
765
  * @param requester The address initiating the transaction
@@ -696,7 +772,7 @@ library EngineBlox {
696
772
  * @param payment The payment details to attach to the record (use empty struct for no payment)
697
773
  * @return TxRecord A new transaction record with populated fields
698
774
  */
699
- function createNewTxRecord(
775
+ function createTxRecord(
700
776
  SecureOperationState storage self,
701
777
  address requester,
702
778
  address target,
@@ -721,7 +797,7 @@ library EngineBlox {
721
797
  executionParams: executionParams
722
798
  }),
723
799
  message: 0,
724
- result: "",
800
+ resultHash: bytes32(0),
725
801
  payment: payment
726
802
  });
727
803
  }
@@ -731,9 +807,8 @@ library EngineBlox {
731
807
  * @param self The SecureOperationState to modify.
732
808
  * @param txId The transaction ID to add to the pending set.
733
809
  */
734
- function addToPendingTransactionsList(SecureOperationState storage self, uint256 txId) private {
735
- SharedValidation.validateTransactionExists(txId);
736
- _validateTxStatus(self, txId, TxStatus.PENDING);
810
+ function addPendingTx(SecureOperationState storage self, uint256 txId) private {
811
+ SharedValidation.validateTransactionExists(txId, self.txCounter);
737
812
 
738
813
  // Try to add transaction ID to the set - add() returns false if already exists
739
814
  if (!self.pendingTransactionsSet.add(txId)) {
@@ -746,8 +821,8 @@ library EngineBlox {
746
821
  * @param self The SecureOperationState to modify.
747
822
  * @param txId The transaction ID to remove from the pending set.
748
823
  */
749
- function removeFromPendingTransactionsList(SecureOperationState storage self, uint256 txId) private {
750
- SharedValidation.validateTransactionExists(txId);
824
+ function removePendingTx(SecureOperationState storage self, uint256 txId) private {
825
+ SharedValidation.validateTransactionExists(txId, self.txCounter);
751
826
 
752
827
  // Remove the transaction ID from the set (O(1) operation)
753
828
  if (!self.pendingTransactionsSet.remove(txId)) {
@@ -786,7 +861,8 @@ library EngineBlox {
786
861
  * @dev Creates a role with specified function permissions.
787
862
  * @param self The SecureOperationState to check.
788
863
  * @param roleName Name of the role.
789
- * @param maxWallets Maximum number of wallets allowed for this role.
864
+ * @param maxWallets Maximum number of wallets allowed for this role. Not capped by a protocol-wide constant;
865
+ * very large values increase gas for `removeRole` and for view helpers that list this role's wallets.
790
866
  * @param isProtected Whether the role is protected from removal.
791
867
  */
792
868
  function createRole(
@@ -831,6 +907,10 @@ library EngineBlox {
831
907
  * @param self The SecureOperationState to modify.
832
908
  * @param roleHash The hash of the role to remove.
833
909
  * @notice Security: Cannot remove protected roles to maintain system integrity.
910
+ * @notice Gas: Iterates all authorized wallets and function selectors on this role (linear in role size).
911
+ * @custom:security PROTECTED-ROLE POLICY: This library enforces the protected-role check for
912
+ * REMOVE_ROLE. RuntimeRBAC does not duplicate this check; defense is in layers. The
913
+ * only component authorized to modify system wallets (protected roles) is SecureOwnable.
834
914
  */
835
915
  function removeRole(
836
916
  SecureOperationState storage self,
@@ -861,19 +941,27 @@ library EngineBlox {
861
941
  self.walletRoles[wallets[i]].remove(roleHash);
862
942
  }
863
943
 
864
- // Clear the role data from roles mapping
865
- // Remove the role from the supported roles set (O(1) operation)
866
- // NOTE: Mappings (functionPermissions, authorizedWallets, functionSelectorsSet)
867
- // are not deleted by Solidity's delete operator. This is acceptable because:
868
- // 1. Role is removed from supportedRolesSet, making it inaccessible via role queries
869
- // 2. Reverse index (walletRoles) is cleaned up above, so permission checks won't find this role
870
- // 3. All access checks use the reverse index (walletRoles) for O(1) lookups, so orphaned data is unreachable
871
- // 4. Role recreation with same name would pass roleHash check but mappings
872
- // would be effectively reset since role is reinitialized from scratch
873
- delete self.roles[roleHash];
944
+ // Clear the role's authorizedWallets set so storage is clean if role is recreated with same name
945
+ for (uint256 i = 0; i < walletCount; i++) {
946
+ roleData.authorizedWallets.remove(wallets[i]);
947
+ }
948
+
949
+ // Clear function permissions and functionSelectorsSet (same reason: no stale data on role recreation)
950
+ uint256 selectorCount = roleData.functionSelectorsSet.length();
951
+ bytes32[] memory selectors = new bytes32[](selectorCount);
952
+ for (uint256 i = 0; i < selectorCount; i++) {
953
+ selectors[i] = roleData.functionSelectorsSet.at(i);
954
+ }
955
+ for (uint256 i = 0; i < selectorCount; i++) {
956
+ roleData.functionSelectorsSet.remove(selectors[i]);
957
+ delete roleData.functionPermissions[bytes4(selectors[i])];
958
+ }
959
+
960
+ // Delete role and remove from supported set (cleanup above ensures no stale RBAC data)
961
+ delete self.roles[roleHash];
874
962
  if (!self.supportedRolesSet.remove(roleHash)) {
875
963
  revert SharedValidation.ResourceNotFound(roleHash);
876
- }
964
+ }
877
965
  }
878
966
 
879
967
  /**
@@ -914,13 +1002,13 @@ library EngineBlox {
914
1002
  }
915
1003
 
916
1004
  /**
917
- * @dev Updates a role from an old address to a new address.
1005
+ * @dev Updates a wallet in a role (replaces oldWallet with newWallet).
918
1006
  * @param self The SecureOperationState to modify.
919
1007
  * @param role The role to update.
920
1008
  * @param newWallet The new wallet address to assign the role to.
921
1009
  * @param oldWallet The old wallet address to remove from the role.
922
1010
  */
923
- function updateAssignedWallet(SecureOperationState storage self, bytes32 role, address newWallet, address oldWallet) public {
1011
+ function updateWallet(SecureOperationState storage self, bytes32 role, address newWallet, address oldWallet) public {
924
1012
  _validateRoleExists(self, role);
925
1013
  SharedValidation.validateNotZeroAddress(newWallet);
926
1014
  SharedValidation.validateNewAddress(newWallet, oldWallet);
@@ -975,6 +1063,10 @@ library EngineBlox {
975
1063
  * @param self The SecureOperationState to modify.
976
1064
  * @param roleHash The role hash to add the function permission to.
977
1065
  * @param functionPermission The function permission to add.
1066
+ * @notice Reverts **`ResourceAlreadyExists`** if the selector is already present on the role. To update
1067
+ * bitmap or `handlerForSelectors`, **`removeFunctionFromRole`** first then re-add.
1068
+ * **`removeFunctionFromRole`** succeeds only when the schema's **`isGrantRevocable`** is true (see there);
1069
+ * **`isProtected`** on the schema does not, by itself, block removing a grant from a role.
978
1070
  */
979
1071
  function addFunctionToRole(
980
1072
  SecureOperationState storage self,
@@ -1007,6 +1099,10 @@ library EngineBlox {
1007
1099
  * @param self The SecureOperationState to modify.
1008
1100
  * @param roleHash The role hash to remove the function permission from.
1009
1101
  * @param functionSelector The function selector to remove from the role.
1102
+ * @notice When the selector is registered, reverts **`GrantNotRevocable`** if **`isGrantRevocable == false`**
1103
+ * (no role may drop this grant). When **`isGrantRevocable == true`**, the grant may be removed from
1104
+ * any role, including protected system roles; **`isProtected`** on the schema still blocks
1105
+ * **`unregisterFunction`** independently, and **`removeRole`** still blocks protected roles.
1010
1106
  */
1011
1107
  function removeFunctionFromRole(
1012
1108
  SecureOperationState storage self,
@@ -1016,12 +1112,10 @@ library EngineBlox {
1016
1112
  // Check if role exists (checks both roles mapping and supportedRolesSet)
1017
1113
  _validateRoleExists(self, roleHash);
1018
1114
 
1019
- // Security check: Prevent removing protected functions from roles
1020
- // Check if function exists and is protected
1021
1115
  if (self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
1022
1116
  FunctionSchema memory functionSchema = self.functions[functionSelector];
1023
- if (functionSchema.isProtected) {
1024
- revert SharedValidation.CannotModifyProtected(bytes32(functionSelector));
1117
+ if (!functionSchema.isGrantRevocable) {
1118
+ revert SharedValidation.GrantNotRevocable(functionSelector);
1025
1119
  }
1026
1120
  }
1027
1121
 
@@ -1040,6 +1134,9 @@ library EngineBlox {
1040
1134
  * @param functionSelector The function selector to check permissions for.
1041
1135
  * @param requestedAction The specific action being requested.
1042
1136
  * @return True if the wallet has permission for the function and action, false otherwise.
1137
+ * @notice Gas scales with the number of roles assigned to `wallet` (reverse index), not with total system roles.
1138
+ * Total distinct roles in the system is bounded by `MAX_ROLES`. Each step uses `functionSelectorsSet.contains`
1139
+ * for the given selector (O(1) in permissions count for that role).
1043
1140
  */
1044
1141
  function hasActionPermission(
1045
1142
  SecureOperationState storage self,
@@ -1047,8 +1144,7 @@ library EngineBlox {
1047
1144
  bytes4 functionSelector,
1048
1145
  TxAction requestedAction
1049
1146
  ) public view returns (bool) {
1050
- // OPTIMIZED: Use reverse index instead of iterating all roles (O(n) -> O(1) lookup)
1051
- // This provides significant gas savings when there are many roles
1147
+ // OPTIMIZED: walletRoles[wallet] lists only this wallet's roles instead of scanning every role in the system.
1052
1148
  EnumerableSet.Bytes32Set storage walletRolesSet = self.walletRoles[wallet];
1053
1149
  uint256 rolesLength = walletRolesSet.length();
1054
1150
 
@@ -1085,6 +1181,10 @@ library EngineBlox {
1085
1181
  * @param functionSelector The function selector to check permissions for.
1086
1182
  * @param requestedAction The specific action being requested.
1087
1183
  * @return True if the role has permission for the function and action, false otherwise.
1184
+ * @notice Uses only whether the role lists `functionSelector` and the action bitmap. It does **not** read
1185
+ * `FunctionPermission.handlerForSelectors` at runtime; that array is validated at **`addFunctionToRole`**
1186
+ * against the function schema (`_validateHandlerForSelectors`). Handler↔execution wiring at use time is
1187
+ * enforced **globally** in strict mode via **`_validateExecutionAndHandlerPermissions`**, not per stored role row.
1088
1188
  */
1089
1189
  function roleHasActionPermission(
1090
1190
  SecureOperationState storage self,
@@ -1107,22 +1207,29 @@ library EngineBlox {
1107
1207
  // ============ FUNCTION MANAGEMENT FUNCTIONS ============
1108
1208
 
1109
1209
  /**
1110
- * @dev Creates a function access control with specified permissions.
1210
+ * @dev Registers a function access control with specified permissions.
1111
1211
  * @param self The SecureOperationState to check.
1112
1212
  * @param functionSignature Function signature (e.g., "transfer(address,uint256)") or function name.
1113
1213
  * @param functionSelector Hash identifier for the function.
1114
1214
  * @param operationName The name of the operation type.
1115
1215
  * @param supportedActionsBitmap Bitmap of permissions required to execute this function.
1116
- * @param isProtected Whether the function schema is protected from removal.
1117
- * @param handlerForSelectors Non-empty array required - execution selectors must contain self-reference, handler selectors must point to execution selectors
1118
- */
1119
- function createFunctionSchema(
1216
+ * @param enforceHandlerRelations When true (strict mode), handlerForSelectors in role permissions must match this schema's handlerForSelectors at use time. When false (flexible mode), forward references are allowed.
1217
+ * @param isProtected Whether the function schema is protected from **unregister** (`unregisterFunction`).
1218
+ * @param isGrantRevocable When false, `removeFunctionFromRole` cannot remove this selector from any role; when true, grants may be removed from any role, including protected system roles.
1219
+ * @param handlerForSelectors Non-empty array required - execution selectors must contain self-reference, handler selectors must point to execution selectors.
1220
+ * @custom:security OPERATIONAL MODES: We do not require handlerForSelectors[i] to be in supportedFunctionsSet at registration.
1221
+ * - Strict mode (enforceHandlerRelations == true): at use time (_validateHandlerForSelectors) we require role permissions' handlerForSelectors to match this schema's handlerForSelectors; registration order is flexible.
1222
+ * - Flexible mode (enforceHandlerRelations == false): validation is skipped; forward references and unregistered selectors are allowed by design. Callers select the mode per schema.
1223
+ */
1224
+ function registerFunction(
1120
1225
  SecureOperationState storage self,
1121
1226
  string memory functionSignature,
1122
1227
  bytes4 functionSelector,
1123
1228
  string memory operationName,
1124
1229
  uint16 supportedActionsBitmap,
1230
+ bool enforceHandlerRelations,
1125
1231
  bool isProtected,
1232
+ bool isGrantRevocable,
1126
1233
  bytes4[] memory handlerForSelectors
1127
1234
  ) public {
1128
1235
  // Validate that functionSignature matches functionSelector
@@ -1132,21 +1239,13 @@ library EngineBlox {
1132
1239
  if (derivedSelector != functionSelector) {
1133
1240
  revert SharedValidation.FunctionSelectorMismatch(functionSelector, derivedSelector);
1134
1241
  }
1135
-
1136
- // SECURITY: Validate that functions existing in contract bytecode must be protected
1137
- // This checks if the function selector exists in the contract's bytecode
1138
- // If it exists, it must be protected to prevent accidental removal of system-critical functions
1139
- _validateContractFunctionProtection(functionSelector, isProtected);
1140
-
1141
1242
  // Derive operation type from operation name
1142
1243
  bytes32 derivedOperationType = keccak256(bytes(operationName));
1143
1244
 
1144
- // Validate handlerForSelectors: non-empty and all selectors are non-zero
1145
- // NOTE:
1146
- // - Empty arrays are NOT allowed anymore. Execution selectors must have
1147
- // at least one entry pointing to themselves (self-reference), and
1148
- // handler selectors must point to valid execution selectors.
1149
- // - bytes4(0) is never allowed in this array.
1245
+ // Validate handlerForSelectors: non-empty and all selectors are non-zero.
1246
+ // We do NOT require handlerForSelectors[i] to be in supportedFunctionsSet here.
1247
+ // Operational mode is controlled by enforceHandlerRelations: strict mode validates at use time;
1248
+ // flexible mode allows forward references and unregistered selectors by design. See @custom:security OPERATIONAL MODES above.
1150
1249
  if (handlerForSelectors.length == 0) {
1151
1250
  revert SharedValidation.OperationFailed();
1152
1251
  }
@@ -1179,7 +1278,9 @@ library EngineBlox {
1179
1278
  schema.operationType = derivedOperationType;
1180
1279
  schema.operationName = operationName;
1181
1280
  schema.supportedActionsBitmap = supportedActionsBitmap;
1281
+ schema.enforceHandlerRelations = enforceHandlerRelations;
1182
1282
  schema.isProtected = isProtected;
1283
+ schema.isGrantRevocable = isGrantRevocable;
1183
1284
  schema.handlerForSelectors = handlerForSelectors;
1184
1285
 
1185
1286
  // Add to supportedFunctionsSet
@@ -1189,15 +1290,24 @@ library EngineBlox {
1189
1290
  }
1190
1291
 
1191
1292
  /**
1192
- * @dev Removes a function schema from the system.
1293
+ * @dev Unregisters a function schema from the system.
1193
1294
  * @param self The SecureOperationState to modify.
1194
- * @param functionSelector The function selector to remove.
1295
+ * @param functionSelector The function selector to unregister.
1195
1296
  * @param safeRemoval If true, reverts with ResourceAlreadyExists when any role still references this function.
1196
1297
  * The safeRemoval check is done inside this function (iterating supportedRolesSet directly) for efficiency.
1197
- * @notice Security: Cannot remove protected function schemas to maintain system integrity.
1298
+ * @notice Security: Cannot unregister protected function schemas to maintain system integrity.
1198
1299
  * @notice Cleanup: Automatically removes unused operation types from supportedOperationTypesSet.
1199
- */
1200
- function removeFunctionSchema(
1300
+ * @notice **Whitelist / hooks:** Per-selector `functionTargetWhitelist` and `functionTargetHooks` are **not**
1301
+ * cleared on unregister. Stale entries are inert while the selector is absent from
1302
+ * `supportedFunctionsSet` (whitelist checks revert `ResourceNotFound`). If the selector is re-registered
1303
+ * later, prior whitelist/hook rows reappear. Operators should clear these via `REMOVE_TARGET_FROM_WHITELIST`
1304
+ * / `clearHook` before or after unregister if a clean slate is desired.
1305
+ * @notice **Role grants with `safeRemoval == false`:** Roles may retain `FunctionPermission` entries for the
1306
+ * unregistered selector; execution paths will revert because the schema no longer exists, but
1307
+ * `roleHasActionPermission` still returns true for the orphan row. Use `safeRemoval == true` or
1308
+ * strip role grants before unregister when state consistency matters.
1309
+ */
1310
+ function unregisterFunction(
1201
1311
  SecureOperationState storage self,
1202
1312
  bytes4 functionSelector,
1203
1313
  bool safeRemoval
@@ -1244,34 +1354,15 @@ library EngineBlox {
1244
1354
  }
1245
1355
  }
1246
1356
 
1247
- /**
1248
- * @dev Checks if a specific action is supported by a function.
1249
- * @param self The SecureOperationState to check.
1250
- * @param functionSelector The function selector to check.
1251
- * @param action The action to check for support.
1252
- * @return True if the action is supported by the function, false otherwise.
1253
- */
1254
- function isActionSupportedByFunction(
1255
- SecureOperationState storage self,
1256
- bytes4 functionSelector,
1257
- TxAction action
1258
- ) public view returns (bool) {
1259
- // Check if function exists in supportedFunctionsSet
1260
- if (!self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
1261
- return false;
1262
- }
1263
-
1264
- FunctionSchema memory functionSchema = self.functions[functionSelector];
1265
- return hasActionInBitmap(functionSchema.supportedActionsBitmap, action);
1266
- }
1267
-
1268
1357
  /**
1269
1358
  * @dev Adds a target address to the whitelist for a function selector.
1270
1359
  * @param self The SecureOperationState to modify.
1271
1360
  * @param functionSelector The function selector whose whitelist will be updated.
1272
1361
  * @param target The target address to add to the whitelist.
1362
+ * @notice There is no on-chain cap on how many targets may be listed per selector (unlike `MAX_HOOKS_PER_SELECTOR`
1363
+ * for hooks). Execution still uses O(1) membership checks; only enumeration helpers scale with set size.
1273
1364
  */
1274
- function addTargetToFunctionWhitelist(
1365
+ function addTargetToWhitelist(
1275
1366
  SecureOperationState storage self,
1276
1367
  bytes4 functionSelector,
1277
1368
  address target
@@ -1295,34 +1386,64 @@ library EngineBlox {
1295
1386
  * @param functionSelector The function selector whose whitelist will be updated.
1296
1387
  * @param target The target address to remove from the whitelist.
1297
1388
  */
1298
- function removeTargetFromFunctionWhitelist(
1389
+ function removeTargetFromWhitelist(
1299
1390
  SecureOperationState storage self,
1300
1391
  bytes4 functionSelector,
1301
1392
  address target
1302
1393
  ) public {
1394
+ SharedValidation.validateNotZeroAddress(target);
1395
+
1303
1396
  EnumerableSet.AddressSet storage set = self.functionTargetWhitelist[functionSelector];
1304
1397
  if (!set.remove(target)) {
1305
1398
  revert SharedValidation.ItemNotFound(target);
1306
1399
  }
1307
1400
  }
1308
1401
 
1402
+ /**
1403
+ * @notice Validates attached `PaymentDetails` at request and execution (whitelist policy).
1404
+ * @dev No-op when both amounts are zero. Otherwise `_validateTargetWhitelist` for
1405
+ * `ATTACHED_PAYMENT_RECIPIENT_SELECTOR` and, when `erc20TokenAmount > 0`, `ERC20_TRANSFER_SELECTOR` (non-zero
1406
+ * target enforced inside `_validateTargetWhitelist`). Invoked from `txRequestWithPayment`, `requestAndApprove`,
1407
+ * and `executeAttachedPayment`.
1408
+ * @param self The secure operation state.
1409
+ * @param payment Attached payment fields.
1410
+ */
1411
+ function _validateAttachedPaymentPolicy(
1412
+ SecureOperationState storage self,
1413
+ PaymentDetails memory payment
1414
+ ) private view {
1415
+ if (payment.nativeTokenAmount == 0 && payment.erc20TokenAmount == 0) {
1416
+ return;
1417
+ }
1418
+ _validateTargetWhitelist(self, ATTACHED_PAYMENT_RECIPIENT_SELECTOR, payment.recipient);
1419
+ if (payment.erc20TokenAmount > 0) {
1420
+ _validateTargetWhitelist(self, ERC20_TRANSFER_SELECTOR, payment.erc20TokenAddress);
1421
+ }
1422
+ }
1423
+
1309
1424
  /**
1310
1425
  * @dev Validates that the target address is whitelisted for the given function selector.
1311
- * Internal contract calls (address(this)) are always allowed.
1426
+ * Reverts if `target` is the zero address (`validateNotZeroAddress`) first.
1427
+ * Reverts if `functionSelector` is not registered in `supportedFunctionsSet` (no silent skip).
1428
+ * Internal contract calls (`target == address(this)`) are always allowed once the selector is registered.
1312
1429
  * @param self The SecureOperationState to check.
1313
1430
  * @param functionSelector The function selector being executed.
1314
1431
  * @param target The target contract address.
1315
1432
  * @notice Target MUST be present in functionTargetWhitelist[functionSelector] unless target is address(this).
1316
1433
  * If whitelist is empty (no entries), no targets are allowed - explicit deny for security.
1434
+ * @notice Attached payments use `ATTACHED_PAYMENT_RECIPIENT_SELECTOR` and `ERC20_TRANSFER_SELECTOR`;
1435
+ * see `_validateAttachedPaymentPolicy`. Deployments using attached payouts must register those schemas.
1436
+ * @notice Gas: Membership check only (`EnumerableSet.contains`); does not scan the full whitelist.
1317
1437
  */
1318
- function _validateFunctionTargetWhitelist(
1438
+ function _validateTargetWhitelist(
1319
1439
  SecureOperationState storage self,
1320
1440
  bytes4 functionSelector,
1321
1441
  address target
1322
1442
  ) internal view {
1323
- // Fast path: selector not registered, skip validation
1443
+ SharedValidation.validateNotZeroAddress(target);
1444
+
1324
1445
  if (!self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
1325
- return;
1446
+ revert SharedValidation.ResourceNotFound(bytes32(functionSelector));
1326
1447
  }
1327
1448
 
1328
1449
  // SECURITY: Internal contract calls are always allowed
@@ -1348,6 +1469,7 @@ library EngineBlox {
1348
1469
  * @param functionSelector The function selector to query.
1349
1470
  * @return Array of whitelisted target addresses.
1350
1471
  * @notice Access control should be enforced by the calling contract.
1472
+ * @notice Gas / return size scale linearly with whitelist length (full set materialization).
1351
1473
  */
1352
1474
  function getFunctionWhitelistTargets(
1353
1475
  SecureOperationState storage self,
@@ -1391,17 +1513,17 @@ library EngineBlox {
1391
1513
  // ============ FUNCTION TARGET HOOKS MANAGEMENT ============
1392
1514
 
1393
1515
  /**
1394
- * @dev Adds a target address to the hooks for a function selector.
1516
+ * @dev Sets (adds) a hook contract for a function selector.
1395
1517
  * @param self The SecureOperationState to modify.
1396
1518
  * @param functionSelector The function selector whose hooks will be updated.
1397
- * @param target The target address to add to the hooks.
1519
+ * @param hook The hook contract address to add (must not be zero).
1398
1520
  */
1399
- function addTargetToFunctionHooks(
1521
+ function setHook(
1400
1522
  SecureOperationState storage self,
1401
1523
  bytes4 functionSelector,
1402
- address target
1524
+ address hook
1403
1525
  ) public {
1404
- SharedValidation.validateNotZeroAddress(target);
1526
+ SharedValidation.validateNotZeroAddress(hook);
1405
1527
 
1406
1528
  // Function selector must be registered in the schema set
1407
1529
  if (!self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
@@ -1416,36 +1538,38 @@ library EngineBlox {
1416
1538
  MAX_HOOKS_PER_SELECTOR
1417
1539
  );
1418
1540
 
1419
- if (!set.add(target)) {
1420
- revert SharedValidation.ItemAlreadyExists(target);
1541
+ if (!set.add(hook)) {
1542
+ revert SharedValidation.ItemAlreadyExists(hook);
1421
1543
  }
1422
1544
  }
1423
1545
 
1424
1546
  /**
1425
- * @dev Removes a target address from the hooks for a function selector.
1547
+ * @dev Clears (removes) a hook contract for a function selector.
1426
1548
  * @param self The SecureOperationState to modify.
1427
1549
  * @param functionSelector The function selector whose hooks will be updated.
1428
- * @param target The target address to remove from the hooks.
1550
+ * @param hook The hook contract address to remove (must not be zero).
1429
1551
  */
1430
- function removeTargetFromFunctionHooks(
1552
+ function clearHook(
1431
1553
  SecureOperationState storage self,
1432
1554
  bytes4 functionSelector,
1433
- address target
1555
+ address hook
1434
1556
  ) public {
1557
+ SharedValidation.validateNotZeroAddress(hook);
1558
+
1435
1559
  EnumerableSet.AddressSet storage set = self.functionTargetHooks[functionSelector];
1436
- if (!set.remove(target)) {
1437
- revert SharedValidation.ItemNotFound(target);
1560
+ if (!set.remove(hook)) {
1561
+ revert SharedValidation.ItemNotFound(hook);
1438
1562
  }
1439
1563
  }
1440
1564
 
1441
1565
  /**
1442
- * @dev Returns all hook target addresses for a function selector.
1566
+ * @dev Returns all configured hooks for a function selector.
1443
1567
  * @param self The SecureOperationState to check.
1444
1568
  * @param functionSelector The function selector to query.
1445
- * @return Array of hook target addresses.
1569
+ * @return Array of hook contract addresses.
1446
1570
  * @notice Access control should be enforced by the calling contract.
1447
1571
  */
1448
- function getFunctionHookTargets(
1572
+ function getHooks(
1449
1573
  SecureOperationState storage self,
1450
1574
  bytes4 functionSelector
1451
1575
  ) public view returns (address[] memory) {
@@ -1469,7 +1593,7 @@ library EngineBlox {
1469
1593
 
1470
1594
  /**
1471
1595
  * @dev Internal: Returns all function schemas that use a specific operation type.
1472
- * Used by removeFunctionSchema and getFunctionsByOperationType.
1596
+ * Used by unregisterFunction and getFunctionsByOperationType.
1473
1597
  */
1474
1598
  function _getFunctionsByOperationType(
1475
1599
  SecureOperationState storage self,
@@ -1500,42 +1624,44 @@ library EngineBlox {
1500
1624
  // ============ BACKWARD COMPATIBILITY FUNCTIONS ============
1501
1625
 
1502
1626
  /**
1503
- * @dev Gets all pending transaction IDs as an array for backward compatibility
1627
+ * @dev Gets all pending transaction IDs
1504
1628
  * @param self The SecureOperationState to check
1505
1629
  * @return Array of pending transaction IDs
1506
1630
  * @notice Access control should be enforced by the calling contract.
1631
+ * @notice Full enumeration of `pendingTransactionsSet`. State-changing engine paths add/remove by id only and
1632
+ * do not walk the entire pending set.
1507
1633
  */
1508
- function getPendingTransactionsList(SecureOperationState storage self) public view returns (uint256[] memory) {
1634
+ function getPendingTransactions(SecureOperationState storage self) public view returns (uint256[] memory) {
1509
1635
  return _convertUintSetToArray(self.pendingTransactionsSet);
1510
1636
  }
1511
1637
 
1512
1638
  /**
1513
- * @dev Gets all supported roles as an array for backward compatibility
1639
+ * @dev Gets all supported roles as an array
1514
1640
  * @param self The SecureOperationState to check
1515
1641
  * @return Array of supported role hashes
1516
1642
  * @notice Access control should be enforced by the calling contract.
1517
1643
  */
1518
- function getSupportedRolesList(SecureOperationState storage self) public view returns (bytes32[] memory) {
1644
+ function getSupportedRoles(SecureOperationState storage self) public view returns (bytes32[] memory) {
1519
1645
  return _convertBytes32SetToArray(self.supportedRolesSet);
1520
1646
  }
1521
1647
 
1522
1648
  /**
1523
- * @dev Gets all supported function selectors as an array for backward compatibility
1649
+ * @dev Gets all supported function selectors as an array
1524
1650
  * @param self The SecureOperationState to check
1525
1651
  * @return Array of supported function selectors
1526
1652
  * @notice Access control should be enforced by the calling contract.
1527
1653
  */
1528
- function getSupportedFunctionsList(SecureOperationState storage self) public view returns (bytes4[] memory) {
1654
+ function getSupportedFunctions(SecureOperationState storage self) public view returns (bytes4[] memory) {
1529
1655
  return _convertBytes4SetToArray(self.supportedFunctionsSet);
1530
1656
  }
1531
1657
 
1532
1658
  /**
1533
- * @dev Gets all supported operation types as an array for backward compatibility
1659
+ * @dev Gets all supported operation types as an array
1534
1660
  * @param self The SecureOperationState to check
1535
1661
  * @return Array of supported operation type hashes
1536
1662
  * @notice Access control should be enforced by the calling contract.
1537
1663
  */
1538
- function getSupportedOperationTypesList(SecureOperationState storage self) public view returns (bytes32[] memory) {
1664
+ function getSupportedOperationTypes(SecureOperationState storage self) public view returns (bytes32[] memory) {
1539
1665
  return _convertBytes32SetToArray(self.supportedOperationTypesSet);
1540
1666
  }
1541
1667
 
@@ -1559,7 +1685,7 @@ library EngineBlox {
1559
1685
  * @return Array of authorized wallet addresses
1560
1686
  * @notice Access control should be enforced by the calling contract.
1561
1687
  */
1562
- function _getAuthorizedWallets(
1688
+ function getAuthorizedWallets(
1563
1689
  SecureOperationState storage self,
1564
1690
  bytes32 roleHash
1565
1691
  ) public view returns (address[] memory) {
@@ -1635,6 +1761,8 @@ library EngineBlox {
1635
1761
  * @param self The SecureOperationState to check against
1636
1762
  * @param metaTx The meta-transaction containing the signature to verify
1637
1763
  * @return True if the signature is valid, false otherwise
1764
+ * @notice Nonce is **per signer** and **strictly sequential** for replay protection (`nonce` must equal `signerNonces[signer]`).
1765
+ * Different signers have independent counters; same-signer submissions must be ordered by relayers—by design.
1638
1766
  */
1639
1767
  function verifySignature(
1640
1768
  SecureOperationState storage self,
@@ -1662,11 +1790,17 @@ library EngineBlox {
1662
1790
  SharedValidation.validateTransactionId(metaTx.txRecord.txId, self.txCounter);
1663
1791
  }
1664
1792
 
1793
+ // Bind signed handlerContract to the verifying contract (EIP-712 verifyingContract).
1794
+ // NOTE: Entrypoint binding to the wrapper selector (msg.sig) must be enforced in the wrapper context
1795
+ // (e.g. BaseStateMachine), not here, because EngineBlox execute via external-library DELEGATECALL.
1796
+ if (metaTx.params.handlerContract != address(this)) {
1797
+ revert SharedValidation.MetaTxHandlerContractMismatch(metaTx.params.handlerContract, address(this));
1798
+ }
1799
+
1665
1800
  // Authorization check - verify signer has meta-transaction signing permissions for the function and action
1666
- 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;
1667
1801
  bool isHandlerAuthorized = hasActionPermission(self, metaTx.params.signer, metaTx.params.handlerSelector, metaTx.params.action);
1668
1802
  bool isExecutionAuthorized = hasActionPermission(self, metaTx.params.signer, metaTx.txRecord.params.executionSelector, metaTx.params.action);
1669
- if (!isSignAction || !isHandlerAuthorized || !isExecutionAuthorized) {
1803
+ if (!isHandlerAuthorized || !isExecutionAuthorized) {
1670
1804
  revert SharedValidation.SignerNotAuthorized(metaTx.params.signer);
1671
1805
  }
1672
1806
 
@@ -1679,39 +1813,84 @@ library EngineBlox {
1679
1813
  }
1680
1814
 
1681
1815
  /**
1682
- * @dev Generates a message hash for the specified meta-transaction following EIP-712
1816
+ * @dev Generates the EIP-712 message hash for the meta-transaction.
1817
+ * Uses selective MetaTxRecord (txId, params, payment only) with standard EIP-712 type hashes
1818
+ * so that eth_signTypedData_v4 (and equivalent) can reproduce the same digest when given
1819
+ * matching domain + types:
1820
+ *
1821
+ * - primaryType: MetaTransaction
1822
+ * - domain: { name: "Bloxchain", version: "1.0.0", chainId, verifyingContract }
1823
+ * - types: MetaTransaction, MetaTxRecord, TxParams, MetaTxParams, PaymentDetails
1824
+ *
1825
+ * Integrators MAY:
1826
+ * - use typed-data signing (eth_signTypedData_v4 / signTypedData) with the above domain/types, or
1827
+ * - sign the resulting digest as a raw hash (e.g. account.sign({ hash: contractDigest })).
1828
+ *
1829
+ * In all cases, on-chain verification uses recoverSigner(messageHash, signature) which applies
1830
+ * ecrecover(messageHash, v, r, s) with no personal_sign / EIP-191 prefix.
1831
+ *
1832
+ * The resulting digest is also written into the `message` field of helper-built `MetaTransaction`
1833
+ * structs so integrators can use it directly without recomputing the hash client-side.
1683
1834
  * @param metaTx The meta-transaction to generate the hash for
1684
- * @return The generated message hash
1835
+ * @return The EIP-712 digest (no prefix; use standard recovery)
1685
1836
  */
1686
1837
  function generateMessageHash(MetaTransaction memory metaTx) private view returns (bytes32) {
1687
- bytes32 domainSeparator = keccak256(abi.encode(
1688
- DOMAIN_SEPARATOR_TYPE_HASH,
1689
- PROTOCOL_NAME_HASH,
1690
- keccak256(abi.encodePacked(VERSION_MAJOR, ".", VERSION_MINOR, ".", VERSION_PATCH)),
1691
- block.chainid,
1692
- address(this)
1838
+ bytes32 domainSeparator = keccak256(
1839
+ abi.encode(
1840
+ DOMAIN_SEPARATOR_TYPE_HASH,
1841
+ PROTOCOL_NAME_HASH,
1842
+ keccak256(bytes(VERSION)),
1843
+ block.chainid,
1844
+ address(this)
1845
+ )
1846
+ );
1847
+
1848
+ TxParams memory tp = metaTx.txRecord.params;
1849
+ bytes32 txParamsStructHash = keccak256(abi.encode(
1850
+ TX_PARAMS_TYPE_HASH,
1851
+ tp.requester,
1852
+ tp.target,
1853
+ tp.value,
1854
+ tp.gasLimit,
1855
+ tp.operationType,
1856
+ tp.executionSelector,
1857
+ keccak256(tp.executionParams)
1858
+ ));
1859
+
1860
+ PaymentDetails memory payment = metaTx.txRecord.payment;
1861
+ bytes32 paymentStructHash = keccak256(abi.encode(
1862
+ PAYMENT_DETAILS_TYPE_HASH,
1863
+ payment.recipient,
1864
+ payment.nativeTokenAmount,
1865
+ payment.erc20TokenAddress,
1866
+ payment.erc20TokenAmount
1867
+ ));
1868
+
1869
+ bytes32 metaTxRecordStructHash = keccak256(abi.encode(
1870
+ META_TX_RECORD_TYPE_HASH,
1871
+ metaTx.txRecord.txId,
1872
+ txParamsStructHash,
1873
+ paymentStructHash
1874
+ ));
1875
+
1876
+ MetaTxParams memory mp = metaTx.params;
1877
+ bytes32 metaTxParamsStructHash = keccak256(abi.encode(
1878
+ META_TX_PARAMS_TYPE_HASH,
1879
+ mp.chainId,
1880
+ mp.nonce,
1881
+ mp.handlerContract,
1882
+ mp.handlerSelector,
1883
+ uint8(mp.action),
1884
+ mp.deadline,
1885
+ mp.maxGasPrice,
1886
+ mp.signer
1693
1887
  ));
1694
1888
 
1695
1889
  bytes32 structHash = keccak256(abi.encode(
1696
- TYPE_HASH,
1697
- keccak256(abi.encode(
1698
- metaTx.txRecord.txId,
1699
- metaTx.txRecord.params.requester,
1700
- metaTx.txRecord.params.target,
1701
- metaTx.txRecord.params.value,
1702
- metaTx.txRecord.params.gasLimit,
1703
- metaTx.txRecord.params.operationType,
1704
- metaTx.txRecord.params.executionSelector,
1705
- keccak256(metaTx.txRecord.params.executionParams)
1706
- )),
1707
- metaTx.params.chainId,
1708
- metaTx.params.nonce,
1709
- metaTx.params.handlerContract,
1710
- metaTx.params.handlerSelector,
1711
- uint8(metaTx.params.action),
1712
- metaTx.params.deadline,
1713
- metaTx.params.maxGasPrice,
1714
- metaTx.params.signer
1890
+ META_TX_TYPE_HASH,
1891
+ metaTxRecordStructHash,
1892
+ metaTxParamsStructHash,
1893
+ keccak256(metaTx.data)
1715
1894
  ));
1716
1895
 
1717
1896
  return keccak256(abi.encodePacked(
@@ -1722,10 +1901,24 @@ library EngineBlox {
1722
1901
  }
1723
1902
 
1724
1903
  /**
1725
- * @dev Recovers the signer address from a message hash and signature.
1726
- * @param messageHash The hash of the message that was signed.
1727
- * @param signature The signature to recover the address from.
1728
- * @return The address of the signer.
1904
+ * @dev Recovers the signer from the EIP-712 digest and signature. Uses standard EIP-712 recovery (no message prefix).
1905
+ *
1906
+ * Integrators have two equivalent options:
1907
+ * - Use typed-data signing (eth_signTypedData_v4 / signTypedData) with:
1908
+ * - primaryType: MetaTransaction
1909
+ * - domain: { name: "Bloxchain", version: "1.0.0", chainId, verifyingContract }
1910
+ * - types: MetaTransaction, MetaTxRecord, TxParams, MetaTxParams, PaymentDetails
1911
+ * In this case the wallet computes the same digest as generateMessageHash and signs it.
1912
+ * - Sign the digest returned by generateMessageHash as a raw hash—e.g.
1913
+ * account.sign({ hash: contractDigest }) or equivalent raw-hash signing API—with no
1914
+ * EIP-191/personal prefix.
1915
+ *
1916
+ * In all cases, this function applies ecrecover(messageHash, v, r, s) over the raw EIP-712 digest.
1917
+ * personal_sign / EIP-191-prefixed signatures remain incompatible.
1918
+ *
1919
+ * @param messageHash The EIP-712 digest (keccak256("\x19\x01" || domainSeparator || structHash))
1920
+ * @param signature The signature (r, s, v)
1921
+ * @return The address of the signer
1729
1922
  */
1730
1923
  function recoverSigner(bytes32 messageHash, bytes memory signature) public pure returns (address) {
1731
1924
  SharedValidation.validateSignatureLength(signature);
@@ -1754,7 +1947,7 @@ library EngineBlox {
1754
1947
  // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}
1755
1948
  SharedValidation.validateSignatureParams(s, v);
1756
1949
 
1757
- address signer = ecrecover(messageHash.toEthSignedMessageHash(), v, r, s);
1950
+ address signer = ecrecover(messageHash, v, r, s);
1758
1951
  SharedValidation.validateRecoveredSigner(signer);
1759
1952
 
1760
1953
  return signer;
@@ -1771,7 +1964,7 @@ library EngineBlox {
1771
1964
  ) public view returns (MetaTransaction memory) {
1772
1965
  SharedValidation.validateNotZeroAddress(txParams.target);
1773
1966
 
1774
- TxRecord memory txRecord = createNewTxRecord(
1967
+ TxRecord memory txRecord = createTxRecord(
1775
1968
  self,
1776
1969
  txParams.requester,
1777
1970
  txParams.target,
@@ -1780,7 +1973,7 @@ library EngineBlox {
1780
1973
  txParams.operationType,
1781
1974
  txParams.executionSelector,
1782
1975
  txParams.executionParams,
1783
- _noPayment()
1976
+ _emptyPayment()
1784
1977
  );
1785
1978
 
1786
1979
  return generateMetaTransaction(self, txRecord, metaTxParams);
@@ -1821,9 +2014,9 @@ library EngineBlox {
1821
2014
  MetaTxParams memory metaTxParams
1822
2015
  ) private view returns (MetaTransaction memory) {
1823
2016
  SharedValidation.validateChainId(metaTxParams.chainId);
1824
- SharedValidation.validateHandlerContract(metaTxParams.handlerContract);
2017
+ SharedValidation.validateMetaTxHandlerContractBinding(metaTxParams.handlerContract);
1825
2018
  SharedValidation.validateHandlerSelector(metaTxParams.handlerSelector);
1826
- SharedValidation.validateDeadline(metaTxParams.deadline);
2019
+ SharedValidation.validateMetaTxDeadline(metaTxParams.deadline);
1827
2020
  SharedValidation.validateNotZeroAddress(metaTxParams.signer);
1828
2021
 
1829
2022
  // Populate the nonce directly from storage for security
@@ -1834,7 +2027,7 @@ library EngineBlox {
1834
2027
  params: metaTxParams,
1835
2028
  message: 0,
1836
2029
  signature: "",
1837
- data: prepareTransactionData(txRecord)
2030
+ data: buildCallData(txRecord)
1838
2031
  });
1839
2032
 
1840
2033
  // Generate the message hash for ready to sign meta-transaction
@@ -1885,6 +2078,16 @@ library EngineBlox {
1885
2078
  * @param self The SecureOperationState
1886
2079
  * @param txId The transaction ID
1887
2080
  * @param functionSelector The function selector to emit in the event
2081
+ * @notice **Trust model:** `eventForwarder` is operator-configured (`setEventForwarder` / init). Treat it as a
2082
+ * **trusted** integration point—malicious or pathological callees can waste gas in the outer tx budget.
2083
+ * @notice **Silent failure:** The `forwardTxEvent` external call is wrapped in `try` / `catch`; reverts or
2084
+ * panics in the forwarder do **not** revert this contract’s state transitions that already completed.
2085
+ * Monitoring cannot assume off-chain delivery succeeded; rely on `TransactionEvent` logs on-chain.
2086
+ * @notice **Gas tradeoff:** There is no explicit `{gas: ...}` stipend; the subcall receives the usual EIP-150
2087
+ * bounded share of remaining gas (not the entire tx). Primary state updates in callers run **before**
2088
+ * `logTxEvent` where applicable. Optional hardening: configurable stipend + explicit failure event.
2089
+ * @notice **Execution returndata:** full bytes are emitted only via `TxExecutionResult` from `_completeTransaction`;
2090
+ * this function emits lifecycle `TransactionEvent` with `resultHash` (zero on request/cancel).
1888
2091
  * @custom:security REENTRANCY PROTECTION: This function is safe from reentrancy because:
1889
2092
  * 1. It is called AFTER all state changes are complete (in _completeTransaction,
1890
2093
  * _cancelTransaction, and txRequest)
@@ -1902,21 +2105,17 @@ library EngineBlox {
1902
2105
  bytes4 functionSelector
1903
2106
  ) public {
1904
2107
  TxRecord memory txRecord = self.txRecords[txId];
1905
-
1906
- // Emit only non-sensitive public data
2108
+
1907
2109
  emit TransactionEvent(
1908
2110
  txId,
1909
2111
  functionSelector,
1910
2112
  txRecord.status,
1911
2113
  txRecord.params.requester,
1912
2114
  txRecord.params.target,
1913
- txRecord.params.operationType
2115
+ txRecord.params.operationType,
2116
+ txRecord.resultHash
1914
2117
  );
1915
-
1916
- // Forward event data to event forwarder
1917
- // REENTRANCY SAFE: External call is wrapped in try-catch and doesn't modify
1918
- // critical state. Even if eventForwarder is malicious, reentry attempts fail
1919
- // because transactions are no longer in PENDING status (they're COMPLETED/CANCELLED).
2118
+
1920
2119
  if (self.eventForwarder != address(0)) {
1921
2120
  try IEventForwarder(self.eventForwarder).forwardTxEvent(
1922
2121
  txId,
@@ -1924,12 +2123,9 @@ library EngineBlox {
1924
2123
  txRecord.status,
1925
2124
  txRecord.params.requester,
1926
2125
  txRecord.params.target,
1927
- txRecord.params.operationType
1928
- ) {
1929
- // Event forwarded successfully
1930
- } catch {
1931
- // Forwarding failed, continue execution (non-critical operation)
1932
- }
2126
+ txRecord.params.operationType,
2127
+ txRecord.resultHash
2128
+ ) {} catch {}
1933
2129
  }
1934
2130
  }
1935
2131
 
@@ -2015,33 +2211,26 @@ library EngineBlox {
2015
2211
  * @param self The SecureOperationState to modify
2016
2212
  * @param txId The transaction ID to complete
2017
2213
  * @param success Whether the transaction execution was successful
2018
- * @param result The result of the transaction execution
2214
+ * @param executionResult Returndata from the main target call (emitted in `TxExecutionResult`).
2019
2215
  */
2020
2216
  function _completeTransaction(
2021
2217
  SecureOperationState storage self,
2022
2218
  uint256 txId,
2023
2219
  bool success,
2024
- bytes memory result
2220
+ bytes memory executionResult
2025
2221
  ) private {
2026
- // enforce that the requested target is whitelisted for this selector.
2027
- _validateFunctionTargetWhitelist(self, self.txRecords[txId].params.executionSelector, self.txRecords[txId].params.target);
2028
-
2029
- // Update storage with new status and result
2222
+ bytes32 resultHash = _executionResultHash(executionResult);
2030
2223
  if (success) {
2031
2224
  self.txRecords[txId].status = TxStatus.COMPLETED;
2032
- self.txRecords[txId].result = result;
2033
2225
  } else {
2034
2226
  self.txRecords[txId].status = TxStatus.FAILED;
2035
- self.txRecords[txId].result = result; // Store failure reason for debugging
2036
- // Note: FAILED status is intentional - transactions can be valid when requested
2037
- // but fail when executed (e.g., conditions changed, insufficient balance, etc.)
2038
- // Users can query status via getTransaction() or listen to TransactionEvent
2039
2227
  }
2040
-
2041
- // Remove from pending transactions list
2042
- removeFromPendingTransactionsList(self, txId);
2043
-
2228
+ self.txRecords[txId].resultHash = resultHash;
2229
+
2230
+ removePendingTx(self, txId);
2231
+
2044
2232
  logTxEvent(self, txId, self.txRecords[txId].params.executionSelector);
2233
+ emit TxExecutionResult(txId, executionResult);
2045
2234
  }
2046
2235
 
2047
2236
  /**
@@ -2053,18 +2242,24 @@ library EngineBlox {
2053
2242
  SecureOperationState storage self,
2054
2243
  uint256 txId
2055
2244
  ) private {
2056
- // enforce that the requested target is whitelisted for this selector.
2057
- _validateFunctionTargetWhitelist(self, self.txRecords[txId].params.executionSelector, self.txRecords[txId].params.target);
2058
-
2059
2245
  self.txRecords[txId].status = TxStatus.CANCELLED;
2060
2246
 
2061
2247
  // Remove from pending transactions list
2062
- removeFromPendingTransactionsList(self, txId);
2248
+ removePendingTx(self, txId);
2063
2249
 
2064
2250
  logTxEvent(self, txId, self.txRecords[txId].params.executionSelector);
2065
2251
  }
2066
2252
 
2067
- /**
2253
+ /**
2254
+ * @dev Commitment to execution returndata for storage and `TransactionEvent.resultHash`.
2255
+ * @param executionResult The execution returndata to hash
2256
+ * @return `bytes32(0)` when empty, else `keccak256(executionResult)`
2257
+ */
2258
+ function _executionResultHash(bytes memory executionResult) private pure returns (bytes32) {
2259
+ return executionResult.length == 0 ? bytes32(0) : keccak256(executionResult);
2260
+ }
2261
+
2262
+ /**
2068
2263
  * @dev Validates that the caller has any role permission
2069
2264
  * @param self The SecureOperationState to check
2070
2265
  * @notice This function consolidates the repeated permission check pattern to reduce contract size
@@ -2123,14 +2318,77 @@ library EngineBlox {
2123
2318
  }
2124
2319
 
2125
2320
  /**
2126
- * @dev Validates that a wallet has permission for both execution selector and handler selector for a given action
2127
- * @param self The SecureOperationState to check
2128
- * @param wallet The wallet address to check permissions for
2129
- * @param executionSelector The execution function selector (underlying operation)
2130
- * @param handlerSelector The handler/calling function selector
2131
- * @param action The action to validate permissions for
2132
- * @notice This function consolidates the repeated dual permission check pattern to reduce contract size
2133
- * @notice Reverts with NoPermission if either permission check fails
2321
+ * @dev Validates that the meta-transaction txRecord matches the stored record for the given txId.
2322
+ * Ensures the signer's intent (as reflected in the stored tx from the request phase) is what
2323
+ * is approved or cancelled; override by meta-tx payload is not allowed for approve/cancel flows.
2324
+ * @param self The SecureOperationState containing the stored tx record.
2325
+ * @param txId The transaction ID.
2326
+ * @param metaTxRecord The TxRecord from the meta-transaction calldata.
2327
+ * @notice Reverts with MetaTxRecordMismatchStoredTx if any execution-affecting or permission-affecting field differs.
2328
+ */
2329
+ function _validateMetaTxMatchRecord(
2330
+ SecureOperationState storage self,
2331
+ uint256 txId,
2332
+ TxRecord memory metaTxRecord
2333
+ ) internal view {
2334
+ TxRecord storage stored = self.txRecords[txId];
2335
+ TxParams storage sp = stored.params;
2336
+ TxParams memory mp = metaTxRecord.params;
2337
+ if (
2338
+ sp.executionSelector != mp.executionSelector ||
2339
+ sp.target != mp.target ||
2340
+ sp.value != mp.value ||
2341
+ sp.requester != mp.requester ||
2342
+ sp.gasLimit != mp.gasLimit ||
2343
+ sp.operationType != mp.operationType ||
2344
+ keccak256(sp.executionParams) != keccak256(mp.executionParams) ||
2345
+ stored.releaseTime != metaTxRecord.releaseTime ||
2346
+ metaTxRecord.resultHash != bytes32(0)
2347
+ ) {
2348
+ revert SharedValidation.MetaTxRecordMismatchStoredTx(txId);
2349
+ }
2350
+ }
2351
+
2352
+ /**
2353
+ * @dev Validates that the meta-transaction payment matches the stored record for the given txId.
2354
+ * Ensures the signed payment (recipient, amounts, token) equals what will be executed.
2355
+ * @param self The SecureOperationState containing the stored tx record.
2356
+ * @param txId The transaction ID.
2357
+ * @param metaTxRecord The TxRecord from the meta-transaction calldata.
2358
+ * @notice Reverts with MetaTxPaymentMismatchStoredTx if any payment field differs from stored.
2359
+ */
2360
+ function _validateMetaTxPaymentMatchRecord(
2361
+ SecureOperationState storage self,
2362
+ uint256 txId,
2363
+ TxRecord memory metaTxRecord
2364
+ ) internal view {
2365
+ PaymentDetails storage storedPayment = self.txRecords[txId].payment;
2366
+ PaymentDetails memory metaPayment = metaTxRecord.payment;
2367
+ if (
2368
+ storedPayment.recipient != metaPayment.recipient ||
2369
+ storedPayment.nativeTokenAmount != metaPayment.nativeTokenAmount ||
2370
+ storedPayment.erc20TokenAddress != metaPayment.erc20TokenAddress ||
2371
+ storedPayment.erc20TokenAmount != metaPayment.erc20TokenAmount
2372
+ ) {
2373
+ revert SharedValidation.MetaTxPaymentMismatchStoredTx(txId);
2374
+ }
2375
+ }
2376
+
2377
+ /**
2378
+ * @dev Validates that a wallet has permission for both execution selector and handler selector for a given action.
2379
+ * @param self The SecureOperationState to check.
2380
+ * @param wallet The wallet address to check permissions for.
2381
+ * @param executionSelector The execution function selector (underlying operation).
2382
+ * @param handlerSelector The handler/calling function selector.
2383
+ * @param action The action to validate permissions for.
2384
+ * @notice This function consolidates the repeated dual permission check pattern to reduce contract size.
2385
+ * @notice Reverts with NoPermission if either permission check fails.
2386
+ * @notice **Strict mode (`enforceHandlerRelations` on the handler schema):** requires `executionSelector` to appear
2387
+ * in **`functions[handlerSelector].handlerForSelectors`** — a **global** graph on the handler’s `FunctionSchema`,
2388
+ * not a lookup of each role’s stored `FunctionPermission.handlerForSelectors` at runtime.
2389
+ * @notice **Role rows:** `FunctionPermission.handlerForSelectors` is enforced when permissions are **granted**
2390
+ * (`addFunctionToRole` → `_validateHandlerForSelectors`); `hasActionPermission` / `roleHasActionPermission` do not
2391
+ * re-apply that list per call. Narrowing is by **which selectors and actions** you grant, plus dual checks here.
2134
2392
  */
2135
2393
  function _validateExecutionAndHandlerPermissions(
2136
2394
  SecureOperationState storage self,
@@ -2139,6 +2397,10 @@ library EngineBlox {
2139
2397
  bytes4 handlerSelector,
2140
2398
  TxAction action
2141
2399
  ) internal view {
2400
+ // Ensure both execution and handler selectors have registered function schemas
2401
+ _validateFunctionSchemaExists(self, executionSelector);
2402
+ _validateFunctionSchemaExists(self, handlerSelector);
2403
+
2142
2404
  // Validate permission for the execution selector (underlying operation)
2143
2405
  if (!hasActionPermission(self, wallet, executionSelector, action)) {
2144
2406
  revert SharedValidation.NoPermission(wallet);
@@ -2147,47 +2409,50 @@ library EngineBlox {
2147
2409
  if (!hasActionPermission(self, wallet, handlerSelector, action)) {
2148
2410
  revert SharedValidation.NoPermission(wallet);
2149
2411
  }
2412
+
2413
+ // In strict mode, enforce that the executionSelector is part of the handlerSelector's schema-level flow.
2414
+ // Handler schemas declare which execution selectors they are allowed to trigger globally. Role grants still
2415
+ // require hasActionPermission on both selectors; per-role FunctionPermission.handlerForSelectors is validated at addFunctionToRole, not re-read here.
2416
+ FunctionSchema storage handlerSchema = self.functions[handlerSelector];
2417
+ if (handlerSchema.enforceHandlerRelations) {
2418
+ if (!_schemaHasHandlerSelector(handlerSchema, executionSelector)) {
2419
+ revert SharedValidation.HandlerForSelectorMismatch(
2420
+ executionSelector,
2421
+ handlerSelector
2422
+ );
2423
+ }
2424
+ }
2150
2425
  }
2151
2426
 
2152
2427
  /**
2153
- * @dev Validates that all handlerForSelectors are present in the schema's handlerForSelectors array
2428
+ * @dev Validates that all handlerForSelectors are present in the schema's handlerForSelectors array.
2429
+ * When schema.enforceHandlerRelations is false (flexible mode), validation is skipped and this function returns immediately.
2430
+ * When true (strict mode), every permission handlerForSelector must appear in the schema's handlerForSelectors.
2154
2431
  * @param self The SecureOperationState to validate against
2155
2432
  * @param functionSelector The function selector for which the permission is defined
2156
2433
  * @param handlerForSelectors The handlerForSelectors array from the permission to validate
2157
- * @notice Reverts with HandlerForSelectorMismatch if any handlerForSelector is not found in the schema's array
2158
- * @notice Special case: Execution function permissions should include functionSelector in handlerForSelectors (self-reference)
2434
+ * @notice Reverts with HandlerForSelectorMismatch if any handlerForSelector is not found in the schema's array (strict mode only).
2435
+ * @notice Special case: Execution function permissions should include functionSelector in handlerForSelectors (self-reference).
2159
2436
  */
2160
2437
  function _validateHandlerForSelectors(
2161
2438
  SecureOperationState storage self,
2162
2439
  bytes4 functionSelector,
2163
2440
  bytes4[] memory handlerForSelectors
2164
2441
  ) internal view {
2165
- bytes32 functionSelectorHash = bytes32(functionSelector);
2166
-
2167
- // Ensure the function schema exists
2168
- if (!self.supportedFunctionsSet.contains(functionSelectorHash)) {
2169
- revert SharedValidation.ResourceNotFound(functionSelectorHash);
2170
- }
2442
+ _validateFunctionSchemaExists(self, functionSelector);
2171
2443
 
2172
2444
  FunctionSchema storage schema = self.functions[functionSelector];
2173
2445
 
2446
+ // If this function schema does not enforce handler relations, skip validation.
2447
+ if (!schema.enforceHandlerRelations) {
2448
+ return;
2449
+ }
2450
+
2174
2451
  // Validate each handlerForSelector in the array
2175
2452
  for (uint256 j = 0; j < handlerForSelectors.length; j++) {
2176
2453
  bytes4 handlerForSelector = handlerForSelectors[j];
2177
-
2178
- // Special case: execution function permissions use handlerForSelector == functionSelector (self-reference)
2179
- if (handlerForSelector == functionSelector) {
2180
- continue; // Valid execution function permission
2181
- }
2182
2454
 
2183
- bool found = false;
2184
- for (uint256 i = 0; i < schema.handlerForSelectors.length; i++) {
2185
- if (schema.handlerForSelectors[i] == handlerForSelector) {
2186
- found = true;
2187
- break;
2188
- }
2189
- }
2190
- if (!found) {
2455
+ if (!_schemaHasHandlerSelector(schema, handlerForSelector)) {
2191
2456
  revert SharedValidation.HandlerForSelectorMismatch(
2192
2457
  bytes4(0), // Cannot return array, use 0 as placeholder
2193
2458
  handlerForSelector
@@ -2196,6 +2461,24 @@ library EngineBlox {
2196
2461
  }
2197
2462
  }
2198
2463
 
2464
+ /**
2465
+ * @dev Checks whether a given handler selector is present in a function schema's handlerForSelectors array.
2466
+ * @param schema The function schema to inspect.
2467
+ * @param handlerSelector The handler selector to search for.
2468
+ * @return True if the handler selector is present, false otherwise.
2469
+ */
2470
+ function _schemaHasHandlerSelector(
2471
+ FunctionSchema storage schema,
2472
+ bytes4 handlerSelector
2473
+ ) internal view returns (bool) {
2474
+ for (uint256 i = 0; i < schema.handlerForSelectors.length; i++) {
2475
+ if (schema.handlerForSelectors[i] == handlerSelector) {
2476
+ return true;
2477
+ }
2478
+ }
2479
+ return false;
2480
+ }
2481
+
2199
2482
  /**
2200
2483
  * @dev Validates meta-transaction permissions for a function permission
2201
2484
  * @param self The secure operation state
@@ -2231,14 +2514,48 @@ library EngineBlox {
2231
2514
  revert SharedValidation.ConflictingMetaTxPermissions(functionPermission.functionSelector);
2232
2515
  }
2233
2516
 
2234
- // Validate that each action in the bitmap is supported by the function
2235
- // This still requires iteration, but we can optimize it
2236
- for (uint i = 0; i < 9; i++) { // TxAction enum has 9 values (0-8)
2237
- if (hasActionInBitmap(bitmap, TxAction(i))) {
2238
- if (!isActionSupportedByFunction(self, functionPermission.functionSelector, TxAction(i))) {
2239
- revert SharedValidation.NotSupported();
2240
- }
2241
- }
2517
+ _validateActionsSupportedByFunction(self, functionPermission.functionSelector, bitmap);
2518
+ }
2519
+
2520
+ /**
2521
+ * @dev Validates that the meta-transaction uses the expected signer action for the current workflow.
2522
+ * @param metaTx The meta-transaction to validate.
2523
+ * @param expectedAction The TxAction that must be used as the signer action.
2524
+ * @custom:security Enforces strict separation between SIGN_META_REQUEST_AND_APPROVE,
2525
+ * SIGN_META_APPROVE and SIGN_META_CANCEL workflows.
2526
+ */
2527
+ function _validateMetaTxAction(
2528
+ MetaTransaction memory metaTx,
2529
+ TxAction expectedAction
2530
+ ) internal pure {
2531
+ if (metaTx.params.action != expectedAction) {
2532
+ revert SharedValidation.NotSupported();
2533
+ }
2534
+ }
2535
+
2536
+ /**
2537
+ * @dev Validates that all actions present in the bitmap are supported by the function schema.
2538
+ * @param self The SecureOperationState to check.
2539
+ * @param functionSelector The function selector to check.
2540
+ * @param bitmap The granted actions bitmap to validate.
2541
+ */
2542
+ function _validateActionsSupportedByFunction(
2543
+ SecureOperationState storage self,
2544
+ bytes4 functionSelector,
2545
+ uint16 bitmap
2546
+ ) internal view {
2547
+ // If the function itself is not supported, none of its actions can be considered valid
2548
+ if (!self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
2549
+ revert SharedValidation.NotSupported();
2550
+ }
2551
+
2552
+ uint16 granted = bitmap;
2553
+ uint16 supported = self.functions[functionSelector].supportedActionsBitmap;
2554
+
2555
+ // Any bit set in granted but not in supported is invalid
2556
+ uint16 invalid = granted & ~supported;
2557
+ if (invalid != 0) {
2558
+ revert SharedValidation.NotSupported();
2242
2559
  }
2243
2560
  }
2244
2561
 
@@ -2246,6 +2563,7 @@ library EngineBlox {
2246
2563
  * @dev Generic helper to convert AddressSet to array
2247
2564
  * @param set The EnumerableSet.AddressSet to convert
2248
2565
  * @return Array of address values
2566
+ * @notice Allocates `length` slots and reads each element—O(set.length()). Used by several public getters.
2249
2567
  */
2250
2568
  function _convertAddressSetToArray(EnumerableSet.AddressSet storage set)
2251
2569
  internal view returns (address[] memory) {
@@ -2302,79 +2620,11 @@ library EngineBlox {
2302
2620
  return result;
2303
2621
  }
2304
2622
 
2305
- /**
2306
- * @dev Validates that if a function exists in contract bytecode, it must be protected
2307
- * @param functionSelector The function selector
2308
- * @param isProtected Whether the function is marked as protected
2309
- * @notice Checks if the function selector exists in the contract's bytecode function selector table
2310
- * @notice If the selector exists in the contract, it must be protected to prevent accidental removal
2311
- * @notice This uses low-level bytecode inspection instead of relying on naming conventions
2312
- * @notice Since we're called via delegatecall, address(this) refers to the calling contract
2313
- */
2314
- function _validateContractFunctionProtection(
2315
- bytes4 functionSelector,
2316
- bool isProtected
2317
- ) private view {
2318
- // Check cheaper condition first: skip expensive bytecode check when already protected
2319
- if (!isProtected) {
2320
- // Check if the function selector exists in the contract's bytecode
2321
- // Since we're called via delegatecall, address(this) refers to the calling contract
2322
- if (selectorExistsInContract(address(this), functionSelector)) {
2323
- revert SharedValidation.ContractFunctionMustBeProtected(functionSelector);
2324
- }
2325
- }
2326
- }
2327
-
2328
- /**
2329
- * @dev Checks if a function selector exists in a contract's bytecode
2330
- * @param contractAddress The address of the contract to check
2331
- * @param selector The 4-byte function selector to search for
2332
- * @return true if the selector is found in the contract's function selector dispatch table area
2333
- * @notice Searches the first 2KB where function selectors are stored in the dispatch table
2334
- * @notice This is a heuristic check - false positives are possible but unlikely
2335
- * @notice Uses loop unrolling for gas efficiency
2336
- * @notice Can be used to query any contract's function selector table
2337
- */
2338
- function selectorExistsInContract(address contractAddress, bytes4 selector) public view returns (bool) {
2339
- // Get the contract's bytecode
2340
- bytes memory code = contractAddress.code;
2341
-
2342
- if (code.length < 5) { // Need at least PUSH4 (1 byte) + selector (4 bytes)
2343
- return false;
2344
- }
2345
-
2346
- // Function selectors are in the dispatch table at the beginning
2347
- // Typical dispatch tables are < 2KB even for large contracts
2348
- // Searching only this area reduces gas cost and false positives from metadata/data
2349
- uint256 searchLength = code.length < 2048 ? code.length : 2048;
2350
-
2351
- // Scan for PUSH4 opcode (0x63) with 1-byte sliding window
2352
- // PUSH4 opcode is followed by 4 bytes which is the function selector
2353
- // Function selectors in EVM bytecode are emitted as PUSH4 <selector> instructions
2354
- // These can start at any byte offset, not just 4-byte-aligned positions
2355
- for (uint256 i = 0; i + 4 < searchLength; i++) {
2356
- // Check if current byte is PUSH4 opcode (0x63)
2357
- if (uint8(code[i]) == 0x63) {
2358
- // Extract the 4-byte selector following the PUSH4 opcode
2359
- bytes4 candidate;
2360
- assembly {
2361
- let codePtr := add(add(code, 0x20), add(i, 1))
2362
- candidate := mload(codePtr)
2363
- }
2364
- if (candidate == selector) {
2365
- return true;
2366
- }
2367
- }
2368
- }
2369
-
2370
- return false;
2371
- }
2372
-
2373
2623
  /**
2374
2624
  * @dev Returns an empty PaymentDetails struct for use when no payment is attached.
2375
2625
  * @return payment Empty payment details (recipient and amounts zero).
2376
2626
  */
2377
- function _noPayment() internal pure returns (PaymentDetails memory payment) {
2627
+ function _emptyPayment() internal pure returns (PaymentDetails memory payment) {
2378
2628
  return PaymentDetails({
2379
2629
  recipient: address(0),
2380
2630
  nativeTokenAmount: 0,