@bloxchain/contracts 1.0.0-alpha.19 → 1.0.0-alpha.20

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.
@@ -16,7 +16,7 @@ import "../../interface/IGuardController.sol";
16
16
  * and role permissions for GuardController's public execution functions.
17
17
  *
18
18
  * Key Features:
19
- * - Registers all 9 GuardController public execution functions
19
+ * - Registers all 9 GuardController public execution functions plus 3 attached-payment policy schemas
20
20
  * - Defines role permissions for OWNER_ROLE and BROADCASTER_ROLE
21
21
  * - Supports time-delay and meta-transaction workflows
22
22
  * - Matches EngineBloxDefinitions pattern for consistency
@@ -34,7 +34,7 @@ library GuardControllerDefinitions {
34
34
  // Operation Type Constants
35
35
  bytes32 public constant CONTROLLER_OPERATION = keccak256("CONTROLLER_OPERATION");
36
36
  // Guard config batch only (whitelist / register-unregister function); distinct execution operation type bitmap.
37
- bytes32 public constant CONTROLLER_CONFIG_OPERATION = keccak256("CONTROLLER_CONFIG_OPERATION");
37
+ bytes32 public constant CONTROLLER_CONFIG_BATCH = keccak256("CONTROLLER_CONFIG_BATCH");
38
38
 
39
39
  // Function Selector Constants
40
40
  // GuardController: executeWithTimeLock(address,uint256,bytes4,bytes,uint256,bytes32)
@@ -83,12 +83,12 @@ library GuardControllerDefinitions {
83
83
  bytes4(keccak256("executeGuardConfigBatch((uint8,bytes)[])"));
84
84
 
85
85
  /**
86
- * @dev Returns predefined function schemas for GuardController execution functions
87
- * @return Array of function schema definitions
86
+ * @dev Returns predefined function schemas for GuardController execution functions and attached-payment policy keys
87
+ * @return Array of function schema definitions (12 entries: 9 controller surfaces + 3 payment whitelist selectors)
88
88
  *
89
89
  * Function schemas define:
90
90
  * - GuardController public execution functions
91
- * - What operation types they belong to (CONTROLLER_OPERATION vs CONTROLLER_CONFIG_OPERATION)
91
+ * - What operation types they belong to (CONTROLLER_OPERATION vs CONTROLLER_CONFIG_BATCH)
92
92
  * - What actions are supported (time-delay request/approve/cancel, meta-tx approve/cancel/request-and-approve)
93
93
  * - Whether they are protected
94
94
  *
@@ -98,7 +98,7 @@ library GuardControllerDefinitions {
98
98
  * - Role permissions are defined in getRolePermissions() matching EngineBloxDefinitions pattern
99
99
  */
100
100
  function getFunctionSchemas() public pure returns (EngineBlox.FunctionSchema[] memory) {
101
- EngineBlox.FunctionSchema[] memory schemas = new EngineBlox.FunctionSchema[](9);
101
+ EngineBlox.FunctionSchema[] memory schemas = new EngineBlox.FunctionSchema[](12);
102
102
 
103
103
  // ============ TIME-DELAY WORKFLOW ACTIONS ============
104
104
  // Request action for executeWithTimeLock
@@ -230,8 +230,8 @@ library GuardControllerDefinitions {
230
230
  schemas[6] = EngineBlox.FunctionSchema({
231
231
  functionSignature: "guardConfigBatchRequestAndApprove(((uint256,uint256,uint8,(address,address,uint256,uint256,bytes32,bytes4,bytes),bytes32,bytes,(address,uint256,address,uint256)),(uint256,uint256,address,bytes4,uint8,uint256,uint256,address),bytes32,bytes,bytes))",
232
232
  functionSelector: GUARD_CONFIG_BATCH_META_SELECTOR,
233
- operationType: CONTROLLER_CONFIG_OPERATION,
234
- operationName: "CONTROLLER_CONFIG_OPERATION",
233
+ operationType: CONTROLLER_CONFIG_BATCH,
234
+ operationName: "CONTROLLER_CONFIG_BATCH",
235
235
  supportedActionsBitmap: EngineBlox.createBitmapFromActions(metaTxRequestApproveActions),
236
236
  enforceHandlerRelations: true,
237
237
  isProtected: true,
@@ -246,16 +246,21 @@ library GuardControllerDefinitions {
246
246
  schemas[7] = EngineBlox.FunctionSchema({
247
247
  functionSignature: "executeGuardConfigBatch((uint8,bytes)[])",
248
248
  functionSelector: GUARD_CONFIG_BATCH_EXECUTE_SELECTOR,
249
- operationType: CONTROLLER_CONFIG_OPERATION,
250
- operationName: "CONTROLLER_CONFIG_OPERATION",
249
+ operationType: CONTROLLER_CONFIG_BATCH,
250
+ operationName: "CONTROLLER_CONFIG_BATCH",
251
251
  supportedActionsBitmap: EngineBlox.createBitmapFromActions(guardConfigExecutionActions),
252
252
  enforceHandlerRelations: false,
253
253
  isProtected: true,
254
254
  handlerForSelectors: guardConfigBatchExecuteHandlerForSelectors
255
255
  });
256
256
 
257
- // Schema 8: GuardController.executeWithPayment (same time-delay request action as executeWithTimeLock;
258
- // OWNER_ROLE grant for this selector may be added manually if the flow is enabled)
257
+ // Schema 8: GuardController.executeWithPayment (same time-delay request action as executeWithTimeLock).
258
+ // Default definitions intentionally omit an OWNER_ROLE FunctionPermission for this selector (minimal surface).
259
+ // `getGuardConfigActionSpecs()` only exposes whitelist add/remove, REGISTER_FUNCTION, and UNREGISTER_FUNCTION —
260
+ // there is no guard-config action to attach `executeWithPayment` to a role. Deployments that need owner-driven
261
+ // `executeWithPayment` must add the FunctionPermission via an RBAC batch (`ADD_FUNCTION_TO_ROLE` / encoders in
262
+ // `RuntimeRBACDefinitions.sol`), following that file's ordering and action constraints and the handler/schema
263
+ // rules in this `GuardControllerDefinitions.sol` bundle.
259
264
  schemas[8] = EngineBlox.FunctionSchema({
260
265
  functionSignature: "executeWithPayment(address,uint256,bytes4,bytes,uint256,bytes32,(address,uint256,address,uint256))",
261
266
  functionSelector: EXECUTE_WITH_PAYMENT_SELECTOR,
@@ -267,6 +272,58 @@ library GuardControllerDefinitions {
267
272
  handlerForSelectors: executeWithPaymentHandlerForSelectors
268
273
  });
269
274
 
275
+ // Policy-only schemas for `executeWithPayment` whitelist keys; bitmap = all TxActions so roles may grant any action if needed.
276
+ EngineBlox.TxAction[] memory allTxActions = new EngineBlox.TxAction[](9);
277
+ allTxActions[0] = EngineBlox.TxAction.EXECUTE_TIME_DELAY_REQUEST;
278
+ allTxActions[1] = EngineBlox.TxAction.EXECUTE_TIME_DELAY_APPROVE;
279
+ allTxActions[2] = EngineBlox.TxAction.EXECUTE_TIME_DELAY_CANCEL;
280
+ allTxActions[3] = EngineBlox.TxAction.SIGN_META_REQUEST_AND_APPROVE;
281
+ allTxActions[4] = EngineBlox.TxAction.SIGN_META_APPROVE;
282
+ allTxActions[5] = EngineBlox.TxAction.SIGN_META_CANCEL;
283
+ allTxActions[6] = EngineBlox.TxAction.EXECUTE_META_REQUEST_AND_APPROVE;
284
+ allTxActions[7] = EngineBlox.TxAction.EXECUTE_META_APPROVE;
285
+ allTxActions[8] = EngineBlox.TxAction.EXECUTE_META_CANCEL;
286
+ uint16 allActionsBitmap = EngineBlox.createBitmapFromActions(allTxActions);
287
+
288
+ bytes4[] memory attachedPaymentRecipientHandlers = new bytes4[](1);
289
+ attachedPaymentRecipientHandlers[0] = EngineBlox.ATTACHED_PAYMENT_RECIPIENT_SELECTOR;
290
+ schemas[9] = EngineBlox.FunctionSchema({
291
+ functionSignature: "__bloxchain_attached_payment_recipient__()",
292
+ functionSelector: EngineBlox.ATTACHED_PAYMENT_RECIPIENT_SELECTOR,
293
+ operationType: keccak256(bytes("ATTACHED_PAYMENT_RECIPIENT")),
294
+ operationName: "ATTACHED_PAYMENT_RECIPIENT",
295
+ supportedActionsBitmap: allActionsBitmap,
296
+ enforceHandlerRelations: false,
297
+ isProtected: true,
298
+ handlerForSelectors: attachedPaymentRecipientHandlers
299
+ });
300
+
301
+ bytes4[] memory nativeTransferHandlers = new bytes4[](1);
302
+ nativeTransferHandlers[0] = EngineBlox.NATIVE_TRANSFER_SELECTOR;
303
+ schemas[10] = EngineBlox.FunctionSchema({
304
+ functionSignature: "__bloxchain_native_transfer__()",
305
+ functionSelector: EngineBlox.NATIVE_TRANSFER_SELECTOR,
306
+ operationType: keccak256(bytes("NATIVE_TRANSFER")),
307
+ operationName: "NATIVE_TRANSFER",
308
+ supportedActionsBitmap: allActionsBitmap,
309
+ enforceHandlerRelations: false,
310
+ isProtected: true,
311
+ handlerForSelectors: nativeTransferHandlers
312
+ });
313
+
314
+ bytes4[] memory erc20TransferHandlers = new bytes4[](1);
315
+ erc20TransferHandlers[0] = EngineBlox.ERC20_TRANSFER_SELECTOR;
316
+ schemas[11] = EngineBlox.FunctionSchema({
317
+ functionSignature: "transfer(address,uint256)",
318
+ functionSelector: EngineBlox.ERC20_TRANSFER_SELECTOR,
319
+ operationType: keccak256(bytes("ERC20_TRANSFER")),
320
+ operationName: "ERC20_TRANSFER",
321
+ supportedActionsBitmap: allActionsBitmap,
322
+ enforceHandlerRelations: false,
323
+ isProtected: true,
324
+ handlerForSelectors: erc20TransferHandlers
325
+ });
326
+
270
327
  return schemas;
271
328
  }
272
329
 
@@ -505,7 +562,7 @@ library GuardControllerDefinitions {
505
562
  /**
506
563
  * @dev Encodes data for UNREGISTER_FUNCTION. Use with GuardConfigActionType.UNREGISTER_FUNCTION.
507
564
  * @param functionSelector Selector of the function to unregister
508
- * @param safeRemoval If true, reverts when the function has whitelisted targets
565
+ * @param safeRemoval If true, `EngineBlox.unregisterFunction` reverts when **any role** still lists this selector (not a whitelist-emptiness check; whitelist/hook entries may remain).
509
566
  */
510
567
  function encodeUnregisterFunction(bytes4 functionSelector, bool safeRemoval) public pure returns (bytes memory) {
511
568
  return abi.encode(functionSelector, safeRemoval);
@@ -28,6 +28,13 @@ import "./interfaces/IEventForwarder.sol";
28
28
  *
29
29
  * The library is designed to be used as a building block for secure smart contract systems
30
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.
31
38
  */
32
39
  library EngineBlox {
33
40
  // ============ VERSION INFORMATION ============
@@ -49,6 +56,11 @@ library EngineBlox {
49
56
 
50
57
  /// @dev Maximum total number of functions allowed in the system (prevents gas exhaustion in function operations)
51
58
  uint256 public constant MAX_FUNCTIONS = 2000;
59
+
60
+ /// @dev Maximum bytes copied from call returndata into memory (and later persisted in `TxRecord.result`).
61
+ /// Full returndata from the callee may be larger; only the first `MAX_RESULT_PREVIEW_BYTES` are retained.
62
+ /// Chosen as 32 KiB to maximize debug/audit surface while bounding memory expansion and storage growth.
63
+ uint256 public constant MAX_RESULT_PREVIEW_BYTES = 32 * 1024;
52
64
 
53
65
  using SharedValidation for *;
54
66
  using EnumerableSet for EnumerableSet.UintSet;
@@ -201,7 +213,14 @@ library EngineBlox {
201
213
 
202
214
  // Native token transfer selector (reserved signature unlikely to exist in real contracts)
203
215
  bytes4 public constant NATIVE_TRANSFER_SELECTOR = bytes4(keccak256("__bloxchain_native_transfer__()"));
204
- bytes32 public constant NATIVE_TRANSFER_OPERATION = keccak256("NATIVE_TRANSFER");
216
+
217
+ /// @dev Reserved pseudo-selector: whitelist key for `PaymentDetails.recipient` (native and ERC20 attached payments).
218
+ /// Function schema is registered by `GuardControllerDefinitions` (and tests/helpers that mirror it), not in `initialize`.
219
+ bytes4 public constant ATTACHED_PAYMENT_RECIPIENT_SELECTOR = bytes4(keccak256("__bloxchain_attached_payment_recipient__()"));
220
+
221
+ /// @dev Standard IERC20 `transfer(address,uint256)`; whitelist key for token contracts used in attached ERC20 payments.
222
+ /// Function schema is registered by `GuardControllerDefinitions` (and tests/helpers that mirror it), not in `initialize`.
223
+ bytes4 public constant ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)"));
205
224
 
206
225
  // EIP-712 Type Hashes (selective meta-tx payload: MetaTxRecord = txId + params + payment only)
207
226
  // These follow the canonical EIP-712 convention so that eth_signTypedData_v4 and equivalent
@@ -311,12 +330,12 @@ library EngineBlox {
311
330
  * @param self The SecureOperationState to modify.
312
331
  * @param requester The address of the requester.
313
332
  * @param target The target contract address for the transaction.
314
- * @param value The value to send with the transaction.
333
+ * @param value The value to send with the transaction (typically 0 for standard calls; non-zero allowed when intentionally forwarding native ETH).
315
334
  * @param gasLimit The gas limit for the transaction.
316
335
  * @param operationType The type of operation.
317
336
  * @param handlerSelector The function selector of the handler/request function.
318
- * @param executionSelector The function selector to execute (NATIVE_TRANSFER_SELECTOR for simple native token transfers).
319
- * @param executionParams The encoded parameters for the function (empty for simple native token transfers).
337
+ * @param executionSelector The function selector to execute (NATIVE_TRANSFER_SELECTOR is a convenience selector for native-only transfers).
338
+ * @param executionParams The encoded parameters for the function (must be empty when using NATIVE_TRANSFER_SELECTOR).
320
339
  * @return The created TxRecord.
321
340
  */
322
341
  function txRequest(
@@ -351,7 +370,7 @@ library EngineBlox {
351
370
  * @param self The SecureOperationState to modify.
352
371
  * @param requester The address of the requester.
353
372
  * @param target The target contract address for the transaction.
354
- * @param value The value to send with the transaction.
373
+ * @param value The value to send with the transaction (typically 0 for standard calls; non-zero allowed when intentionally forwarding native ETH).
355
374
  * @param gasLimit The gas limit for the transaction.
356
375
  * @param operationType The type of operation.
357
376
  * @param handlerSelector The function selector of the handler/request function.
@@ -376,15 +395,7 @@ library EngineBlox {
376
395
  // Validate both execution and handler selector permissions (same as txRequest)
377
396
  _validateExecutionAndHandlerPermissions(self, msg.sender, executionSelector, handlerSelector, TxAction.EXECUTE_TIME_DELAY_REQUEST);
378
397
 
379
- // Request-time validation for attached payment details.
380
- // This prevents creating persistent PENDING records that later fail during
381
- // `executeAttachedPayment` due to missing/zero payment fields.
382
- if (paymentDetails.nativeTokenAmount > 0 || paymentDetails.erc20TokenAmount > 0) {
383
- SharedValidation.validateNotZeroAddress(paymentDetails.recipient);
384
- }
385
- if (paymentDetails.erc20TokenAmount > 0) {
386
- SharedValidation.validateNotZeroAddress(paymentDetails.erc20TokenAddress);
387
- }
398
+ _validateAttachedPaymentPolicy(self, paymentDetails);
388
399
 
389
400
  return _txRequest(
390
401
  self,
@@ -424,8 +435,7 @@ library EngineBlox {
424
435
  bytes memory executionParams,
425
436
  PaymentDetails memory paymentDetails
426
437
  ) private returns (TxRecord memory) {
427
- SharedValidation.validateNotZeroAddress(target);
428
- // enforce that the requested target is whitelisted for this selector.
438
+ // Target non-zero and whitelist enforced in `_validateTargetWhitelist`.
429
439
  _validateTargetWhitelist(self, executionSelector, target);
430
440
 
431
441
  TxRecord memory txRequestRecord = createTxRecord(
@@ -583,16 +593,8 @@ library EngineBlox {
583
593
 
584
594
  // Validate both execution and handler selector permissions
585
595
  _validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_REQUEST_AND_APPROVE);
586
-
587
- // Request-time validation for attached payment details.
588
- // `requestAndApprove` creates the request and executes via the same meta-tx flow,
589
- // so we validate here to avoid persisting bad PENDING records.
590
- if (metaTx.txRecord.payment.nativeTokenAmount > 0 || metaTx.txRecord.payment.erc20TokenAmount > 0) {
591
- SharedValidation.validateNotZeroAddress(metaTx.txRecord.payment.recipient);
592
- }
593
- if (metaTx.txRecord.payment.erc20TokenAmount > 0) {
594
- SharedValidation.validateNotZeroAddress(metaTx.txRecord.payment.erc20TokenAddress);
595
- }
596
+
597
+ _validateAttachedPaymentPolicy(self, metaTx.txRecord.payment);
596
598
 
597
599
  TxRecord memory txRecord = _txRequest(
598
600
  self,
@@ -610,6 +612,46 @@ library EngineBlox {
610
612
  return _txApprovalWithMetaTx(self, metaTx);
611
613
  }
612
614
 
615
+ /**
616
+ * @dev Performs `address.call` without copying unbounded returndata into memory.
617
+ * @param target Callee address
618
+ * @param value Native value to forward
619
+ * @param callGas Gas to forward to the callee
620
+ * @param data Full calldata for the call
621
+ * @return success Whether the call returned success
622
+ * @return result First `min(returndatasize(), MAX_RESULT_PREVIEW_BYTES)` bytes of returndata
623
+ */
624
+ function _callWithBoundedReturndata(
625
+ address target,
626
+ uint256 value,
627
+ uint256 callGas,
628
+ bytes memory data
629
+ ) private returns (bool success, bytes memory result) {
630
+ uint256 dataLength = data.length;
631
+ assembly ("memory-safe") {
632
+ success := call(
633
+ callGas,
634
+ target,
635
+ value,
636
+ add(data, 0x20),
637
+ dataLength,
638
+ 0,
639
+ 0
640
+ )
641
+ }
642
+ uint256 returnSize;
643
+ assembly ("memory-safe") {
644
+ returnSize := returndatasize()
645
+ }
646
+ uint256 copyLength = returnSize > MAX_RESULT_PREVIEW_BYTES ? MAX_RESULT_PREVIEW_BYTES : returnSize;
647
+ result = new bytes(copyLength);
648
+ assembly ("memory-safe") {
649
+ if gt(copyLength, 0) {
650
+ returndatacopy(add(result, 0x20), 0, copyLength)
651
+ }
652
+ }
653
+ }
654
+
613
655
  /**
614
656
  * @dev Executes a transaction based on its execution type and attached payment.
615
657
  * @param self The SecureOperationState storage reference (for validation)
@@ -624,6 +666,13 @@ library EngineBlox {
624
666
  * causing _validateTxPending to revert in entry functions
625
667
  * 4. Status flow is one-way: PENDING → EXECUTING → (COMPLETED/FAILED)
626
668
  * This creates an effective reentrancy guard without additional storage overhead.
669
+ * @notice Returndata from the target is captured up to `MAX_RESULT_PREVIEW_BYTES` only (low-level `call` with
670
+ * zero output area, then bounded `returndatacopy`). This prevents unbounded memory expansion from
671
+ * malicious or buggy callees while preserving a large preview for debugging and audits.
672
+ * @notice **Atomicity:** If the main call succeeds but `executeAttachedPayment` reverts (e.g.
673
+ * insufficient balance, whitelist mismatch), the **entire** approval/execute transaction reverts—main
674
+ * effect included. This is **intentional all-or-nothing** semantics; splitting finalize vs payment
675
+ * would require a separate design with reentrancy and state-machine implications.
627
676
  */
628
677
  function executeTransaction(SecureOperationState storage self, TxRecord memory record) private returns (bool, bytes memory) {
629
678
  // Validate that transaction is in EXECUTING status (set by caller before this function)
@@ -631,6 +680,8 @@ library EngineBlox {
631
680
  _validateTxStatus(self, record.txId, TxStatus.EXECUTING);
632
681
 
633
682
  bytes memory txData = buildCallData(record);
683
+ // gasLimit == 0 → forward all remaining gas (conventional "no cap"). Integrators that want
684
+ // a strict upper bound must set a positive gasLimit in TxParams at request time.
634
685
  uint gas = record.params.gasLimit;
635
686
  if (gas == 0) {
636
687
  gas = gasleft();
@@ -639,7 +690,10 @@ library EngineBlox {
639
690
  // Execute the main transaction
640
691
  // REENTRANCY SAFE: Status is EXECUTING, preventing reentry through entry functions
641
692
  // that require PENDING status. Any reentry attempt would fail at _validateTxStatus(..., PENDING).
642
- (bool success, bytes memory result) = record.params.target.call{value: record.params.value, gas: gas}(
693
+ (bool success, bytes memory result) = _callWithBoundedReturndata(
694
+ record.params.target,
695
+ record.params.value,
696
+ gas,
643
697
  txData
644
698
  );
645
699
 
@@ -672,6 +726,13 @@ library EngineBlox {
672
726
  * 4. All entry functions check for PENDING status first, so reentry fails
673
727
  * The external calls (native token transfer, ERC20 transfer) cannot reenter
674
728
  * critical functions because the transaction is no longer in PENDING state.
729
+ * @notice When payment amounts are non-zero, `payment.recipient` is validated against
730
+ * `ATTACHED_PAYMENT_RECIPIENT_SELECTOR`; non-zero ERC20 amounts also require
731
+ * `payment.erc20TokenAddress` on `ERC20_TRANSFER_SELECTOR` whitelist.
732
+ * @notice **ERC20 attached payouts** use `safeTransfer` with the **nominal** `erc20TokenAmount`.
733
+ * **Fee-on-transfer, rebasing, or other non-standard ERC20s are not supported:** the protocol does not
734
+ * measure balance deltas at the recipient; operators must only attach **standard** tokens where
735
+ * transferred amount equals the requested amount.
675
736
  */
676
737
  function executeAttachedPayment(
677
738
  SecureOperationState storage self,
@@ -680,6 +741,9 @@ library EngineBlox {
680
741
  // Validate that transaction is still in EXECUTING status
681
742
  // This ensures reentrancy protection is maintained throughout payment execution
682
743
  _validateTxStatus(self, record.txId, TxStatus.EXECUTING);
744
+
745
+ _validateAttachedPaymentPolicy(self, record.payment);
746
+
683
747
  self.txRecords[record.txId].status = TxStatus.PROCESSING_PAYMENT;
684
748
 
685
749
  PaymentDetails memory payment = record.payment;
@@ -840,7 +904,8 @@ library EngineBlox {
840
904
  * @dev Creates a role with specified function permissions.
841
905
  * @param self The SecureOperationState to check.
842
906
  * @param roleName Name of the role.
843
- * @param maxWallets Maximum number of wallets allowed for this role.
907
+ * @param maxWallets Maximum number of wallets allowed for this role. Not capped by a protocol-wide constant;
908
+ * very large values increase gas for `removeRole` and for view helpers that list this role's wallets.
844
909
  * @param isProtected Whether the role is protected from removal.
845
910
  */
846
911
  function createRole(
@@ -885,6 +950,7 @@ library EngineBlox {
885
950
  * @param self The SecureOperationState to modify.
886
951
  * @param roleHash The hash of the role to remove.
887
952
  * @notice Security: Cannot remove protected roles to maintain system integrity.
953
+ * @notice Gas: Iterates all authorized wallets and function selectors on this role (linear in role size).
888
954
  * @custom:security PROTECTED-ROLE POLICY: This library enforces the protected-role check for
889
955
  * REMOVE_ROLE. RuntimeRBAC does not duplicate this check; defense is in layers. The
890
956
  * only component authorized to modify system wallets (protected roles) is SecureOwnable.
@@ -1040,6 +1106,10 @@ library EngineBlox {
1040
1106
  * @param self The SecureOperationState to modify.
1041
1107
  * @param roleHash The role hash to add the function permission to.
1042
1108
  * @param functionPermission The function permission to add.
1109
+ * @notice Reverts **`ResourceAlreadyExists`** if the selector is already present on the role. To update
1110
+ * bitmap or `handlerForSelectors`, **`removeFunctionFromRole`** first then re-add.
1111
+ * Protected schemas cannot be removed from roles (`CannotModifyProtected`), so grants of
1112
+ * protected selectors to a role are effectively **permanent** unless the role itself is removed.
1043
1113
  */
1044
1114
  function addFunctionToRole(
1045
1115
  SecureOperationState storage self,
@@ -1072,6 +1142,10 @@ library EngineBlox {
1072
1142
  * @param self The SecureOperationState to modify.
1073
1143
  * @param roleHash The role hash to remove the function permission from.
1074
1144
  * @param functionSelector The function selector to remove from the role.
1145
+ * @notice **Protected schemas cannot be removed from roles** (`CannotModifyProtected`). Granting a protected
1146
+ * selector to a role is therefore an **irreversible expansion** of that role's capability unless the
1147
+ * role itself is removed. Operators should only add protected selectors to roles when the
1148
+ * permanent authority is intended.
1075
1149
  */
1076
1150
  function removeFunctionFromRole(
1077
1151
  SecureOperationState storage self,
@@ -1105,6 +1179,9 @@ library EngineBlox {
1105
1179
  * @param functionSelector The function selector to check permissions for.
1106
1180
  * @param requestedAction The specific action being requested.
1107
1181
  * @return True if the wallet has permission for the function and action, false otherwise.
1182
+ * @notice Gas scales with the number of roles assigned to `wallet` (reverse index), not with total system roles.
1183
+ * Total distinct roles in the system is bounded by `MAX_ROLES`. Each step uses `functionSelectorsSet.contains`
1184
+ * for the given selector (O(1) in permissions count for that role).
1108
1185
  */
1109
1186
  function hasActionPermission(
1110
1187
  SecureOperationState storage self,
@@ -1112,8 +1189,7 @@ library EngineBlox {
1112
1189
  bytes4 functionSelector,
1113
1190
  TxAction requestedAction
1114
1191
  ) public view returns (bool) {
1115
- // OPTIMIZED: Use reverse index instead of iterating all roles (O(n) -> O(1) lookup)
1116
- // This provides significant gas savings when there are many roles
1192
+ // OPTIMIZED: walletRoles[wallet] lists only this wallet's roles instead of scanning every role in the system.
1117
1193
  EnumerableSet.Bytes32Set storage walletRolesSet = self.walletRoles[wallet];
1118
1194
  uint256 rolesLength = walletRolesSet.length();
1119
1195
 
@@ -1150,6 +1226,10 @@ library EngineBlox {
1150
1226
  * @param functionSelector The function selector to check permissions for.
1151
1227
  * @param requestedAction The specific action being requested.
1152
1228
  * @return True if the role has permission for the function and action, false otherwise.
1229
+ * @notice Uses only whether the role lists `functionSelector` and the action bitmap. It does **not** read
1230
+ * `FunctionPermission.handlerForSelectors` at runtime; that array is validated at **`addFunctionToRole`**
1231
+ * against the function schema (`_validateHandlerForSelectors`). Handler↔execution wiring at use time is
1232
+ * enforced **globally** in strict mode via **`_validateExecutionAndHandlerPermissions`**, not per stored role row.
1153
1233
  */
1154
1234
  function roleHasActionPermission(
1155
1235
  SecureOperationState storage self,
@@ -1259,6 +1339,15 @@ library EngineBlox {
1259
1339
  * The safeRemoval check is done inside this function (iterating supportedRolesSet directly) for efficiency.
1260
1340
  * @notice Security: Cannot unregister protected function schemas to maintain system integrity.
1261
1341
  * @notice Cleanup: Automatically removes unused operation types from supportedOperationTypesSet.
1342
+ * @notice **Whitelist / hooks:** Per-selector `functionTargetWhitelist` and `functionTargetHooks` are **not**
1343
+ * cleared on unregister. Stale entries are inert while the selector is absent from
1344
+ * `supportedFunctionsSet` (whitelist checks revert `ResourceNotFound`). If the selector is re-registered
1345
+ * later, prior whitelist/hook rows reappear. Operators should clear these via `REMOVE_TARGET_FROM_WHITELIST`
1346
+ * / `clearHook` before or after unregister if a clean slate is desired.
1347
+ * @notice **Role grants with `safeRemoval == false`:** Roles may retain `FunctionPermission` entries for the
1348
+ * unregistered selector; execution paths will revert because the schema no longer exists, but
1349
+ * `roleHasActionPermission` still returns true for the orphan row. Use `safeRemoval == true` or
1350
+ * strip role grants before unregister when state consistency matters.
1262
1351
  */
1263
1352
  function unregisterFunction(
1264
1353
  SecureOperationState storage self,
@@ -1312,6 +1401,8 @@ library EngineBlox {
1312
1401
  * @param self The SecureOperationState to modify.
1313
1402
  * @param functionSelector The function selector whose whitelist will be updated.
1314
1403
  * @param target The target address to add to the whitelist.
1404
+ * @notice There is no on-chain cap on how many targets may be listed per selector (unlike `MAX_HOOKS_PER_SELECTOR`
1405
+ * for hooks). Execution still uses O(1) membership checks; only enumeration helpers scale with set size.
1315
1406
  */
1316
1407
  function addTargetToWhitelist(
1317
1408
  SecureOperationState storage self,
@@ -1350,23 +1441,51 @@ library EngineBlox {
1350
1441
  }
1351
1442
  }
1352
1443
 
1444
+ /**
1445
+ * @notice Validates attached `PaymentDetails` at request and execution (whitelist policy).
1446
+ * @dev No-op when both amounts are zero. Otherwise `_validateTargetWhitelist` for
1447
+ * `ATTACHED_PAYMENT_RECIPIENT_SELECTOR` and, when `erc20TokenAmount > 0`, `ERC20_TRANSFER_SELECTOR` (non-zero
1448
+ * target enforced inside `_validateTargetWhitelist`). Invoked from `txRequestWithPayment`, `requestAndApprove`,
1449
+ * and `executeAttachedPayment`.
1450
+ * @param self The secure operation state.
1451
+ * @param payment Attached payment fields.
1452
+ */
1453
+ function _validateAttachedPaymentPolicy(
1454
+ SecureOperationState storage self,
1455
+ PaymentDetails memory payment
1456
+ ) private view {
1457
+ if (payment.nativeTokenAmount == 0 && payment.erc20TokenAmount == 0) {
1458
+ return;
1459
+ }
1460
+ _validateTargetWhitelist(self, ATTACHED_PAYMENT_RECIPIENT_SELECTOR, payment.recipient);
1461
+ if (payment.erc20TokenAmount > 0) {
1462
+ _validateTargetWhitelist(self, ERC20_TRANSFER_SELECTOR, payment.erc20TokenAddress);
1463
+ }
1464
+ }
1465
+
1353
1466
  /**
1354
1467
  * @dev Validates that the target address is whitelisted for the given function selector.
1355
- * Internal contract calls (address(this)) are always allowed.
1468
+ * Reverts if `target` is the zero address (`validateNotZeroAddress`) first.
1469
+ * Reverts if `functionSelector` is not registered in `supportedFunctionsSet` (no silent skip).
1470
+ * Internal contract calls (`target == address(this)`) are always allowed once the selector is registered.
1356
1471
  * @param self The SecureOperationState to check.
1357
1472
  * @param functionSelector The function selector being executed.
1358
1473
  * @param target The target contract address.
1359
1474
  * @notice Target MUST be present in functionTargetWhitelist[functionSelector] unless target is address(this).
1360
1475
  * If whitelist is empty (no entries), no targets are allowed - explicit deny for security.
1476
+ * @notice Attached payments use `ATTACHED_PAYMENT_RECIPIENT_SELECTOR` and `ERC20_TRANSFER_SELECTOR`;
1477
+ * see `_validateAttachedPaymentPolicy`. Deployments using attached payouts must register those schemas.
1478
+ * @notice Gas: Membership check only (`EnumerableSet.contains`); does not scan the full whitelist.
1361
1479
  */
1362
1480
  function _validateTargetWhitelist(
1363
1481
  SecureOperationState storage self,
1364
1482
  bytes4 functionSelector,
1365
1483
  address target
1366
1484
  ) internal view {
1367
- // Fast path: selector not registered, skip validation
1485
+ SharedValidation.validateNotZeroAddress(target);
1486
+
1368
1487
  if (!self.supportedFunctionsSet.contains(bytes32(functionSelector))) {
1369
- return;
1488
+ revert SharedValidation.ResourceNotFound(bytes32(functionSelector));
1370
1489
  }
1371
1490
 
1372
1491
  // SECURITY: Internal contract calls are always allowed
@@ -1392,6 +1511,7 @@ library EngineBlox {
1392
1511
  * @param functionSelector The function selector to query.
1393
1512
  * @return Array of whitelisted target addresses.
1394
1513
  * @notice Access control should be enforced by the calling contract.
1514
+ * @notice Gas / return size scale linearly with whitelist length (full set materialization).
1395
1515
  */
1396
1516
  function getFunctionWhitelistTargets(
1397
1517
  SecureOperationState storage self,
@@ -1550,6 +1670,8 @@ library EngineBlox {
1550
1670
  * @param self The SecureOperationState to check
1551
1671
  * @return Array of pending transaction IDs
1552
1672
  * @notice Access control should be enforced by the calling contract.
1673
+ * @notice Full enumeration of `pendingTransactionsSet`. State-changing engine paths add/remove by id only and
1674
+ * do not walk the entire pending set.
1553
1675
  */
1554
1676
  function getPendingTransactions(SecureOperationState storage self) public view returns (uint256[] memory) {
1555
1677
  return _convertUintSetToArray(self.pendingTransactionsSet);
@@ -1681,6 +1803,8 @@ library EngineBlox {
1681
1803
  * @param self The SecureOperationState to check against
1682
1804
  * @param metaTx The meta-transaction containing the signature to verify
1683
1805
  * @return True if the signature is valid, false otherwise
1806
+ * @notice Nonce is **per signer** and **strictly sequential** for replay protection (`nonce` must equal `signerNonces[signer]`).
1807
+ * Different signers have independent counters; same-signer submissions must be ordered by relayers—by design.
1684
1808
  */
1685
1809
  function verifySignature(
1686
1810
  SecureOperationState storage self,
@@ -1708,6 +1832,13 @@ library EngineBlox {
1708
1832
  SharedValidation.validateTransactionId(metaTx.txRecord.txId, self.txCounter);
1709
1833
  }
1710
1834
 
1835
+ // Bind signed handlerContract to the verifying contract (EIP-712 verifyingContract).
1836
+ // NOTE: Entrypoint binding to the wrapper selector (msg.sig) must be enforced in the wrapper context
1837
+ // (e.g. BaseStateMachine), not here, because EngineBlox execute via external-library DELEGATECALL.
1838
+ if (metaTx.params.handlerContract != address(this)) {
1839
+ revert SharedValidation.MetaTxHandlerContractMismatch(metaTx.params.handlerContract, address(this));
1840
+ }
1841
+
1711
1842
  // Authorization check - verify signer has meta-transaction signing permissions for the function and action
1712
1843
  bool isHandlerAuthorized = hasActionPermission(self, metaTx.params.signer, metaTx.params.handlerSelector, metaTx.params.action);
1713
1844
  bool isExecutionAuthorized = hasActionPermission(self, metaTx.params.signer, metaTx.txRecord.params.executionSelector, metaTx.params.action);
@@ -1925,7 +2056,7 @@ library EngineBlox {
1925
2056
  MetaTxParams memory metaTxParams
1926
2057
  ) private view returns (MetaTransaction memory) {
1927
2058
  SharedValidation.validateChainId(metaTxParams.chainId);
1928
- SharedValidation.validateHandlerContract(metaTxParams.handlerContract);
2059
+ SharedValidation.validateMetaTxHandlerContractBinding(metaTxParams.handlerContract);
1929
2060
  SharedValidation.validateHandlerSelector(metaTxParams.handlerSelector);
1930
2061
  SharedValidation.validateDeadline(metaTxParams.deadline);
1931
2062
  SharedValidation.validateNotZeroAddress(metaTxParams.signer);
@@ -1989,6 +2120,14 @@ library EngineBlox {
1989
2120
  * @param self The SecureOperationState
1990
2121
  * @param txId The transaction ID
1991
2122
  * @param functionSelector The function selector to emit in the event
2123
+ * @notice **Trust model:** `eventForwarder` is operator-configured (`setEventForwarder` / init). Treat it as a
2124
+ * **trusted** integration point—malicious or pathological callees can waste gas in the outer tx budget.
2125
+ * @notice **Silent failure:** The `forwardTxEvent` external call is wrapped in `try` / `catch`; reverts or
2126
+ * panics in the forwarder do **not** revert this contract’s state transitions that already completed.
2127
+ * Monitoring cannot assume off-chain delivery succeeded; rely on `TransactionEvent` logs on-chain.
2128
+ * @notice **Gas tradeoff:** There is no explicit `{gas: ...}` stipend; the subcall receives the usual EIP-150
2129
+ * bounded share of remaining gas (not the entire tx). Primary state updates in callers run **before**
2130
+ * `logTxEvent` where applicable. Optional hardening: configurable stipend + explicit failure event.
1992
2131
  * @custom:security REENTRANCY PROTECTION: This function is safe from reentrancy because:
1993
2132
  * 1. It is called AFTER all state changes are complete (in _completeTransaction,
1994
2133
  * _cancelTransaction, and txRequest)
@@ -2291,8 +2430,12 @@ library EngineBlox {
2291
2430
  * @param action The action to validate permissions for.
2292
2431
  * @notice This function consolidates the repeated dual permission check pattern to reduce contract size.
2293
2432
  * @notice Reverts with NoPermission if either permission check fails.
2294
- * @notice Strict mode enforces that the handler's *schema-level* handlerForSelectors flow allows the execution selector;
2295
- * it does not bind this relation to a specific role's FunctionPermission, which may further narrow pairings.
2433
+ * @notice **Strict mode (`enforceHandlerRelations` on the handler schema):** requires `executionSelector` to appear
2434
+ * in **`functions[handlerSelector].handlerForSelectors`** a **global** graph on the handler’s `FunctionSchema`,
2435
+ * not a lookup of each role’s stored `FunctionPermission.handlerForSelectors` at runtime.
2436
+ * @notice **Role rows:** `FunctionPermission.handlerForSelectors` is enforced when permissions are **granted**
2437
+ * (`addFunctionToRole` → `_validateHandlerForSelectors`); `hasActionPermission` / `roleHasActionPermission` do not
2438
+ * re-apply that list per call. Narrowing is by **which selectors and actions** you grant, plus dual checks here.
2296
2439
  */
2297
2440
  function _validateExecutionAndHandlerPermissions(
2298
2441
  SecureOperationState storage self,
@@ -2315,8 +2458,8 @@ library EngineBlox {
2315
2458
  }
2316
2459
 
2317
2460
  // In strict mode, enforce that the executionSelector is part of the handlerSelector's schema-level flow.
2318
- // Handler schemas declare which execution selectors they are allowed to trigger globally; role permissions
2319
- // can still narrow which selectors a given wallet may use via FunctionPermission.handlerForSelectors.
2461
+ // Handler schemas declare which execution selectors they are allowed to trigger globally. Role grants still
2462
+ // require hasActionPermission on both selectors; per-role FunctionPermission.handlerForSelectors is validated at addFunctionToRole, not re-read here.
2320
2463
  FunctionSchema storage handlerSchema = self.functions[handlerSelector];
2321
2464
  if (handlerSchema.enforceHandlerRelations) {
2322
2465
  if (!_schemaHasHandlerSelector(handlerSchema, executionSelector)) {
@@ -2467,6 +2610,7 @@ library EngineBlox {
2467
2610
  * @dev Generic helper to convert AddressSet to array
2468
2611
  * @param set The EnumerableSet.AddressSet to convert
2469
2612
  * @return Array of address values
2613
+ * @notice Allocates `length` slots and reads each element—O(set.length()). Used by several public getters.
2470
2614
  */
2471
2615
  function _convertAddressSetToArray(EnumerableSet.AddressSet storage set)
2472
2616
  internal view returns (address[] memory) {