@bloxchain/contracts 1.0.0-alpha.16 → 1.0.0-alpha.17

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.
@@ -349,6 +349,28 @@
349
349
  "name": "MetaTxExpired",
350
350
  "type": "error"
351
351
  },
352
+ {
353
+ "inputs": [
354
+ {
355
+ "internalType": "uint256",
356
+ "name": "txId",
357
+ "type": "uint256"
358
+ }
359
+ ],
360
+ "name": "MetaTxPaymentMismatchStoredTx",
361
+ "type": "error"
362
+ },
363
+ {
364
+ "inputs": [
365
+ {
366
+ "internalType": "uint256",
367
+ "name": "txId",
368
+ "type": "uint256"
369
+ }
370
+ ],
371
+ "name": "MetaTxRecordMismatchStoredTx",
372
+ "type": "error"
373
+ },
352
374
  {
353
375
  "inputs": [
354
376
  {
@@ -12,18 +12,28 @@ import "./interface/IRuntimeRBAC.sol";
12
12
  /**
13
13
  * @title RuntimeRBAC
14
14
  * @dev Minimal Runtime Role-Based Access Control system based on EngineBlox
15
- *
15
+ *
16
16
  * This contract provides essential runtime RBAC functionality:
17
17
  * - Creation of non-protected roles
18
18
  * - Basic wallet assignment to roles
19
19
  * - Function permission management per role
20
20
  * - Integration with EngineBlox for secure operations
21
- *
21
+ *
22
22
  * Key Features:
23
23
  * - Only non-protected roles can be created dynamically
24
24
  * - Protected roles (OWNER, BROADCASTER, RECOVERY) are managed by SecureOwnable
25
25
  * - Minimal interface for core RBAC operations
26
26
  * - Essential role management functions only
27
+ *
28
+ * @custom:security PROTECTED-ROLE POLICY (defense in layers):
29
+ * - RuntimeRBAC is **unauthorized** to modify protected roles (wallet add/revoke/remove).
30
+ * - For ADD_WALLET and REVOKE_WALLET we call _requireRoleNotProtected so batch ops cannot
31
+ * change who holds system roles. For REMOVE_ROLE we rely on EngineBlox.removeRole, which
32
+ * enforces the same policy at the library layer (cannot remove protected roles).
33
+ * - The **only** place to modify system wallets (protected roles) is the SecureOwnable
34
+ * security component (e.g. transferOwnershipRequest, broadcaster/recovery changes).
35
+ * - This layering is intentional: RBAC cannot touch protected roles; SecureOwnable is the
36
+ * single source of truth for system wallet changes.
27
37
  */
28
38
  abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
29
39
  using EngineBlox for EngineBlox.SecureOperationState;
@@ -71,7 +81,7 @@ abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
71
81
  /**
72
82
  * @dev Requests and approves a RBAC configuration batch using a meta-transaction
73
83
  * @param metaTx The meta-transaction
74
- * @return The transaction record
84
+ * @return The transaction ID of the applied batch
75
85
  * @notice OWNER signs, BROADCASTER executes according to RuntimeRBACDefinitions
76
86
  */
77
87
  function roleConfigBatchRequestAndApprove(
@@ -85,6 +95,13 @@ abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
85
95
  /**
86
96
  * @dev External function that can only be called by the contract itself to execute a RBAC configuration batch
87
97
  * @param actions Encoded role configuration actions
98
+ *
99
+ * ## Role config batch ordering (required to avoid revert and gas waste)
100
+ *
101
+ * Actions must be ordered so that dependencies are satisfied:
102
+ * - **CREATE_ROLE** must appear before **ADD_WALLET** or **ADD_FUNCTION_TO_ROLE** for the same role; otherwise the role does not exist and the add will revert.
103
+ * - **REMOVE_ROLE** should be used only for an existing role; use **REVOKE_WALLET** first if the role has assigned wallets (optional but recommended for clarity).
104
+ * - For a given role, typical order: CREATE_ROLE → ADD_WALLET / ADD_FUNCTION_TO_ROLE as needed; to remove: REVOKE_WALLET (and REMOVE_FUNCTION_FROM_ROLE) as needed → REMOVE_ROLE.
88
105
  */
89
106
  function executeRoleConfigBatch(IRuntimeRBAC.RoleConfigAction[] calldata actions) external {
90
107
  _validateExecuteBySelf();
@@ -95,6 +112,9 @@ abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
95
112
 
96
113
  /**
97
114
  * @dev Reverts if the role is protected (prevents editing OWNER, BROADCASTER, RECOVERY via batch).
115
+ * Used for ADD_WALLET and REVOKE_WALLET so RuntimeRBAC cannot change who holds system roles.
116
+ * REMOVE_ROLE is not checked here; EngineBlox.removeRole enforces protected-role policy at
117
+ * the library layer. See contract-level @custom:security PROTECTED-ROLE POLICY.
98
118
  * @param roleHash The role hash to check
99
119
  */
100
120
  function _requireRoleNotProtected(bytes32 roleHash) internal view {
@@ -106,6 +126,10 @@ abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
106
126
  /**
107
127
  * @dev Internal helper to execute a RBAC configuration batch
108
128
  * @param actions Encoded role configuration actions
129
+ *
130
+ * @custom:order Required ordering to avoid revert and gas waste:
131
+ * 1. CREATE_ROLE before any ADD_WALLET or ADD_FUNCTION_TO_ROLE for that role.
132
+ * 2. REMOVE_ROLE only for a role that exists; prefer REVOKE_WALLET (and REMOVE_FUNCTION_FROM_ROLE) before REMOVE_ROLE when the role has members.
109
133
  */
110
134
  function _executeRoleConfigBatch(IRuntimeRBAC.RoleConfigAction[] calldata actions) internal {
111
135
  _validateBatchSize(actions.length);
@@ -142,7 +166,11 @@ abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
142
166
  }
143
167
 
144
168
  /**
145
- * @dev Executes REMOVE_ROLE: removes a role by hash
169
+ * @dev Executes REMOVE_ROLE: removes a role by hash.
170
+ * Protected-role check is enforced in EngineBlox.removeRole (library layer); RuntimeRBAC
171
+ * does not duplicate it here. SecureOwnable is the only component authorized to change
172
+ * system wallets; RBAC is unauthorized to modify protected roles. See @custom:security
173
+ * PROTECTED-ROLE POLICY on the contract.
146
174
  * @param data ABI-encoded (bytes32 roleHash)
147
175
  */
148
176
  function _executeRemoveRole(bytes calldata data) internal {
@@ -174,8 +202,11 @@ abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
174
202
  }
175
203
 
176
204
  /**
177
- * @dev Executes ADD_FUNCTION_TO_ROLE: adds a function permission to a role
205
+ * @dev Executes ADD_FUNCTION_TO_ROLE: adds a function permission to a role.
178
206
  * @param data ABI-encoded (bytes32 roleHash, FunctionPermission functionPermission)
207
+ * @custom:security By design we allow adding function permissions to protected roles (OWNER, BROADCASTER, RECOVERY)
208
+ * to retain flexibility to grant new function permissions to system roles; only wallet add/revoke
209
+ * are restricted on protected roles.
179
210
  */
180
211
  function _executeAddFunctionToRole(bytes calldata data) internal {
181
212
  (
@@ -187,8 +218,11 @@ abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
187
218
  }
188
219
 
189
220
  /**
190
- * @dev Executes REMOVE_FUNCTION_FROM_ROLE: removes a function permission from a role
221
+ * @dev Executes REMOVE_FUNCTION_FROM_ROLE: removes a function permission from a role.
191
222
  * @param data ABI-encoded (bytes32 roleHash, bytes4 functionSelector)
223
+ * @custom:security By design we allow removing function permissions from protected roles (OWNER, BROADCASTER, RECOVERY)
224
+ * to retain flexibility to adjust which functions system roles can call; only wallet add/revoke
225
+ * are restricted on protected roles.
192
226
  */
193
227
  function _executeRemoveFunctionFromRole(bytes calldata data) internal {
194
228
  (bytes32 roleHash, bytes4 functionSelector) = abi.decode(data, (bytes32, bytes4));
@@ -12,7 +12,7 @@ import "../../lib/EngineBlox.sol";
12
12
  *
13
13
  * Key Features:
14
14
  * - Batch-based role configuration (atomic operations)
15
- * - Runtime function schema registration
15
+ * - Role and permission management (function schema registration is handled by GuardController)
16
16
  * - Integration with EngineBlox for secure operations
17
17
  * - Query functions for role and permission inspection
18
18
  *
@@ -47,7 +47,7 @@ interface IRuntimeRBAC {
47
47
  /**
48
48
  * @dev Requests and approves a RBAC configuration batch using a meta-transaction
49
49
  * @param metaTx The meta-transaction
50
- * @return The transaction record
50
+ * @return The transaction ID of the applied batch
51
51
  */
52
52
  function roleConfigBatchRequestAndApprove(
53
53
  EngineBlox.MetaTransaction memory metaTx
@@ -520,6 +520,7 @@ abstract contract BaseStateMachine is Initializable, ERC165Upgradeable, Reentran
520
520
  * @dev Gets function schema information
521
521
  * @param functionSelector The function selector to get information for
522
522
  * @return The full FunctionSchema struct (functionSignature, functionSelector, operationType, operationName, supportedActionsBitmap, enforceHandlerRelations, isProtected, handlerForSelectors)
523
+ * @notice Reverts with ResourceNotFound if the schema does not exist
523
524
  */
524
525
  function getFunctionSchema(bytes4 functionSelector) external view returns (EngineBlox.FunctionSchema memory) {
525
526
  _validateAnyRole();
@@ -32,7 +32,7 @@ import "./interface/IGuardController.sol";
32
32
  *
33
33
  * Usage Flow:
34
34
  * 1. Deploy GuardController (or combine with RuntimeRBAC/SecureOwnable for role management)
35
- * 2. Function schemas should be registered via definitions or RuntimeRBAC if combined
35
+ * 2. Function schemas are registered via definitions at init or via GuardController guard config batch (REGISTER_FUNCTION)
36
36
  * 3. Create roles and assign function permissions with action bitmaps (via RuntimeRBAC if combined)
37
37
  * 4. Assign wallets to roles (via RuntimeRBAC if combined)
38
38
  * 5. Configure target whitelists per function selector (REQUIRED for execution)
@@ -173,7 +173,7 @@ abstract contract GuardController is BaseStateMachine {
173
173
  /**
174
174
  * @dev Approves and executes a time-locked transaction
175
175
  * @param txId The transaction ID
176
- * @return result The execution result
176
+ * @return txId The transaction ID
177
177
  * @notice Requires STANDARD execution type and EXECUTE_TIME_DELAY_APPROVE permission for the execution function
178
178
  */
179
179
  function approveTimeLockExecution(
@@ -191,7 +191,7 @@ abstract contract GuardController is BaseStateMachine {
191
191
  /**
192
192
  * @dev Cancels a time-locked transaction
193
193
  * @param txId The transaction ID
194
- * @return The updated transaction record
194
+ * @return The transaction ID
195
195
  * @notice Requires STANDARD execution type and EXECUTE_TIME_DELAY_CANCEL permission for the execution function
196
196
  */
197
197
  function cancelTimeLockExecution(
@@ -209,7 +209,7 @@ abstract contract GuardController is BaseStateMachine {
209
209
  /**
210
210
  * @dev Approves a time-locked transaction using a meta-transaction
211
211
  * @param metaTx The meta-transaction containing the transaction record and signature
212
- * @return The updated transaction record
212
+ * @return The transaction ID
213
213
  * @notice Requires STANDARD execution type and EXECUTE_META_APPROVE permission for the execution function
214
214
  */
215
215
  function approveTimeLockExecutionWithMetaTx(
@@ -226,7 +226,7 @@ abstract contract GuardController is BaseStateMachine {
226
226
  /**
227
227
  * @dev Cancels a time-locked transaction using a meta-transaction
228
228
  * @param metaTx The meta-transaction containing the transaction record and signature
229
- * @return The updated transaction record
229
+ * @return The transaction ID
230
230
  * @notice Requires STANDARD execution type and EXECUTE_META_CANCEL permission for the execution function
231
231
  */
232
232
  function cancelTimeLockExecutionWithMetaTx(
@@ -243,7 +243,7 @@ abstract contract GuardController is BaseStateMachine {
243
243
  /**
244
244
  * @dev Requests and approves a transaction in one step using a meta-transaction
245
245
  * @param metaTx The meta-transaction containing the transaction record and signature
246
- * @return The transaction record after request and approval
246
+ * @return The transaction ID
247
247
  * @notice Requires STANDARD execution type
248
248
  * @notice Validates function schema and permissions for the execution function (same as executeWithTimeLock)
249
249
  * @notice Requires EXECUTE_META_REQUEST_AND_APPROVE permission for the execution function selector
@@ -304,7 +304,7 @@ abstract contract GuardController is BaseStateMachine {
304
304
  /**
305
305
  * @dev Requests and approves a Guard configuration batch using a meta-transaction
306
306
  * @param metaTx The meta-transaction
307
- * @return The transaction record
307
+ * @return The transaction ID
308
308
  * @notice OWNER signs, BROADCASTER executes according to GuardControllerDefinitions
309
309
  */
310
310
  function guardConfigBatchRequestAndApprove(
@@ -5,10 +5,10 @@ import "../../lib/EngineBlox.sol";
5
5
 
6
6
  /**
7
7
  * @title IGuardController
8
- * @dev Interface for GuardController contract that GuardianSafeV3 and other contracts delegate to
8
+ * @dev Interface for GuardController contract that AccountBlox and other contracts delegate to
9
9
  * @notice This interface defines only GuardController-specific methods
10
- * @notice Functions from BaseStateMachine (createMetaTxParams, generateUnsignedMetaTransaction*, getTransaction, functionSchemaExists, getFunctionSchema, owner, getBroadcaster, getRecovery) should be accessed via IBaseStateMachine
11
- * @notice Functions from RuntimeRBAC (registerFunction, unregisterFunction, createNewRole, addWalletToRole, revokeWallet) should be accessed via IRuntimeRBAC
10
+ * @notice Functions from BaseStateMachine (createMetaTxParams, generateUnsignedMetaTransaction*, getTransaction, getFunctionSchema, owner, getBroadcasters, getRecovery) should be accessed via IBaseStateMachine
11
+ * @notice Functions from RuntimeRBAC (role management: createNewRole, addWalletToRole, revokeWallet, etc.) should be accessed via IRuntimeRBAC. Function schema registration is performed via GuardController (guard config batch), not RuntimeRBAC.
12
12
  * @custom:security-contact security@particlecrypto.com
13
13
  */
14
14
  interface IGuardController {
@@ -3,7 +3,6 @@ pragma solidity 0.8.34;
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
@@ -53,7 +52,6 @@ library EngineBlox {
53
52
  /// @dev Maximum total number of functions allowed in the system (prevents gas exhaustion in function operations)
54
53
  uint256 public constant MAX_FUNCTIONS = 2000;
55
54
 
56
- using MessageHashUtils for bytes32;
57
55
  using SharedValidation for *;
58
56
  using EnumerableSet for EnumerableSet.UintSet;
59
57
  using EnumerableSet for EnumerableSet.Bytes32Set;
@@ -152,9 +150,11 @@ library EngineBlox {
152
150
  bytes32 operationType;
153
151
  string operationName;
154
152
  uint16 supportedActionsBitmap; // Bitmap for TxAction enum (9 bits max)
155
- bool enforceHandlerRelations; // When true, handlerForSelectors in permissions must match schema.handlerForSelectors (except self-reference)
153
+ /// @dev When true (strict mode): handlerForSelectors in role permissions must match this schema's handlerForSelectors at use time.
154
+ /// When false (flexible mode): no such check; forward references and unregistered selectors in handlerForSelectors are allowed at registration.
155
+ bool enforceHandlerRelations;
156
156
  bool isProtected;
157
- bytes4[] handlerForSelectors;
157
+ bytes4[] handlerForSelectors;
158
158
  }
159
159
 
160
160
  // ============ DEFINITION STRUCTS ============
@@ -205,8 +205,12 @@ library EngineBlox {
205
205
  bytes4 public constant NATIVE_TRANSFER_SELECTOR = bytes4(keccak256("__bloxchain_native_transfer__()"));
206
206
  bytes32 public constant NATIVE_TRANSFER_OPERATION = keccak256("NATIVE_TRANSFER");
207
207
 
208
- // EIP-712 Type Hashes
209
- 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)");
208
+ // EIP-712 Type Hashes (selective meta-tx payload: MetaTxRecord = txId + params + payment only)
209
+ bytes32 private constant META_TX_TYPE_HASH = keccak256("MetaTransaction(MetaTxRecord txRecord,MetaTxParams params,bytes data)MetaTxRecord(uint256 txId,TxParams params,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)");
210
+ bytes32 private constant META_TX_RECORD_TYPE_HASH = keccak256("MetaTxRecord(uint256 txId,TxParams params,PaymentDetails payment)TxParams(address requester,address target,uint256 value,uint256 gasLimit,bytes32 operationType,bytes4 executionSelector,bytes executionParams)PaymentDetails(address recipient,uint256 nativeTokenAmount,address erc20TokenAddress,uint256 erc20TokenAmount)");
211
+ bytes32 private constant TX_PARAMS_TYPE_HASH = keccak256("TxParams(address requester,address target,uint256 value,uint256 gasLimit,bytes32 operationType,bytes4 executionSelector,bytes executionParams)");
212
+ 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)");
213
+ bytes32 private constant PAYMENT_DETAILS_TYPE_HASH = keccak256("PaymentDetails(address recipient,uint256 nativeTokenAmount,address erc20TokenAddress,uint256 erc20TokenAmount)");
210
214
  bytes32 private constant DOMAIN_SEPARATOR_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
211
215
 
212
216
 
@@ -483,6 +487,8 @@ library EngineBlox {
483
487
  // Validate both execution and handler selector permissions
484
488
  _validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_CANCEL);
485
489
  _validateTxStatus(self, txId, TxStatus.PENDING);
490
+ _validateMetaTxMatchRecord(self, txId, metaTx.txRecord);
491
+ _validateMetaTxPaymentMatchRecord(self, txId, metaTx.txRecord);
486
492
  if (!verifySignature(self, metaTx)) revert SharedValidation.InvalidSignature(metaTx.signature);
487
493
 
488
494
  incrementSignerNonce(self, metaTx.params.signer);
@@ -513,10 +519,16 @@ library EngineBlox {
513
519
  * @return The updated TxRecord.
514
520
  * @notice This function skips permission validation and should only be called from functions
515
521
  * that have already validated permissions.
522
+ * @custom:security TIMELOCK: The releaseTime (timelock) is intentionally NOT enforced on this path.
523
+ * This is by design: the meta-tx workflow allows authorized signers to approve execution
524
+ * without waiting for releaseTime, providing a hybrid synergy between timelock workflows
525
+ * (direct path enforces releaseTime) and meta-tx workflows (delegated, time-flexible approval).
516
526
  */
517
527
  function _txApprovalWithMetaTx(SecureOperationState storage self, MetaTransaction memory metaTx) private returns (TxRecord memory) {
518
528
  uint256 txId = metaTx.txRecord.txId;
519
529
  _validateTxStatus(self, txId, TxStatus.PENDING);
530
+ _validateMetaTxMatchRecord(self, txId, metaTx.txRecord);
531
+ _validateMetaTxPaymentMatchRecord(self, txId, metaTx.txRecord);
520
532
  if (!verifySignature(self, metaTx)) revert SharedValidation.InvalidSignature(metaTx.signature);
521
533
 
522
534
  incrementSignerNonce(self, metaTx.params.signer);
@@ -838,6 +850,9 @@ library EngineBlox {
838
850
  * @param self The SecureOperationState to modify.
839
851
  * @param roleHash The hash of the role to remove.
840
852
  * @notice Security: Cannot remove protected roles to maintain system integrity.
853
+ * @custom:security PROTECTED-ROLE POLICY: This library enforces the protected-role check for
854
+ * REMOVE_ROLE. RuntimeRBAC does not duplicate this check; defense is in layers. The
855
+ * only component authorized to modify system wallets (protected roles) is SecureOwnable.
841
856
  */
842
857
  function removeRole(
843
858
  SecureOperationState storage self,
@@ -868,19 +883,27 @@ library EngineBlox {
868
883
  self.walletRoles[wallets[i]].remove(roleHash);
869
884
  }
870
885
 
871
- // Clear the role data from roles mapping
872
- // Remove the role from the supported roles set (O(1) operation)
873
- // NOTE: Mappings (functionPermissions, authorizedWallets, functionSelectorsSet)
874
- // are not deleted by Solidity's delete operator. This is acceptable because:
875
- // 1. Role is removed from supportedRolesSet, making it inaccessible via role queries
876
- // 2. Reverse index (walletRoles) is cleaned up above, so permission checks won't find this role
877
- // 3. All access checks use the reverse index (walletRoles) for O(1) lookups, so orphaned data is unreachable
878
- // 4. Role recreation with same name would pass roleHash check but mappings
879
- // would be effectively reset since role is reinitialized from scratch
880
- delete self.roles[roleHash];
886
+ // Clear the role's authorizedWallets set so storage is clean if role is recreated with same name
887
+ for (uint256 i = 0; i < walletCount; i++) {
888
+ roleData.authorizedWallets.remove(wallets[i]);
889
+ }
890
+
891
+ // Clear function permissions and functionSelectorsSet (same reason: no stale data on role recreation)
892
+ uint256 selectorCount = roleData.functionSelectorsSet.length();
893
+ bytes32[] memory selectors = new bytes32[](selectorCount);
894
+ for (uint256 i = 0; i < selectorCount; i++) {
895
+ selectors[i] = roleData.functionSelectorsSet.at(i);
896
+ }
897
+ for (uint256 i = 0; i < selectorCount; i++) {
898
+ roleData.functionSelectorsSet.remove(selectors[i]);
899
+ delete roleData.functionPermissions[bytes4(selectors[i])];
900
+ }
901
+
902
+ // Delete role and remove from supported set (cleanup above ensures no stale RBAC data)
903
+ delete self.roles[roleHash];
881
904
  if (!self.supportedRolesSet.remove(roleHash)) {
882
905
  revert SharedValidation.ResourceNotFound(roleHash);
883
- }
906
+ }
884
907
  }
885
908
 
886
909
  /**
@@ -1120,9 +1143,12 @@ library EngineBlox {
1120
1143
  * @param functionSelector Hash identifier for the function.
1121
1144
  * @param operationName The name of the operation type.
1122
1145
  * @param supportedActionsBitmap Bitmap of permissions required to execute this function.
1123
- * @param enforceHandlerRelations When true, handlerForSelectors in permissions must match schema.handlerForSelectors (except self-reference).
1146
+ * @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.
1124
1147
  * @param isProtected Whether the function schema is protected from removal.
1125
- * @param handlerForSelectors Non-empty array required - execution selectors must contain self-reference, handler selectors must point to execution selectors
1148
+ * @param handlerForSelectors Non-empty array required - execution selectors must contain self-reference, handler selectors must point to execution selectors.
1149
+ * @custom:security OPERATIONAL MODES: We do not require handlerForSelectors[i] to be in supportedFunctionsSet at registration.
1150
+ * - Strict mode (enforceHandlerRelations == true): at use time (_validateHandlerForSelectors) we require role permissions' handlerForSelectors to match this schema's handlerForSelectors; registration order is flexible.
1151
+ * - Flexible mode (enforceHandlerRelations == false): validation is skipped; forward references and unregistered selectors are allowed by design. Callers select the mode per schema.
1126
1152
  */
1127
1153
  function registerFunction(
1128
1154
  SecureOperationState storage self,
@@ -1144,12 +1170,10 @@ library EngineBlox {
1144
1170
  // Derive operation type from operation name
1145
1171
  bytes32 derivedOperationType = keccak256(bytes(operationName));
1146
1172
 
1147
- // Validate handlerForSelectors: non-empty and all selectors are non-zero
1148
- // NOTE:
1149
- // - Empty arrays are NOT allowed anymore. Execution selectors must have
1150
- // at least one entry pointing to themselves (self-reference), and
1151
- // handler selectors must point to valid execution selectors.
1152
- // - bytes4(0) is never allowed in this array.
1173
+ // Validate handlerForSelectors: non-empty and all selectors are non-zero.
1174
+ // We do NOT require handlerForSelectors[i] to be in supportedFunctionsSet here.
1175
+ // Operational mode is controlled by enforceHandlerRelations: strict mode validates at use time;
1176
+ // flexible mode allows forward references and unregistered selectors by design. See @custom:security OPERATIONAL MODES above.
1153
1177
  if (handlerForSelectors.length == 0) {
1154
1178
  revert SharedValidation.OperationFailed();
1155
1179
  }
@@ -1665,9 +1689,19 @@ library EngineBlox {
1665
1689
  }
1666
1690
 
1667
1691
  /**
1668
- * @dev Generates a message hash for the specified meta-transaction following EIP-712
1692
+ * @dev Generates the EIP-712 message hash for the meta-transaction.
1693
+ * Uses selective MetaTxRecord (txId, params, payment only).
1694
+ * Integrators must sign this digest as a raw hash with no EIP-191 or personal_sign prefix—
1695
+ * e.g. account.sign({ hash: contractDigest }) or equivalent raw-hash signing API—so that
1696
+ * signatures match the raw ecrecover(messageHash, v, r, s) verification in recoverSigner.
1697
+ * Do NOT use personal_sign or generic eth_signTypedData_v4; the contract uses
1698
+ * abi.encodePacked for the version string and a custom META_TX_TYPE_HASH, so those produce
1699
+ * incompatible hashes.
1700
+ * The resulting digest is also written into the `message` field of the helper-built
1701
+ * `MetaTransaction` structs (see `createMetaTransactionForSigning`) so integrators can use
1702
+ * it directly without recomputing the hash client-side.
1669
1703
  * @param metaTx The meta-transaction to generate the hash for
1670
- * @return The generated message hash
1704
+ * @return The EIP-712 digest (no prefix; use standard recovery)
1671
1705
  */
1672
1706
  function generateMessageHash(MetaTransaction memory metaTx) private view returns (bytes32) {
1673
1707
  bytes32 domainSeparator = keccak256(abi.encode(
@@ -1678,26 +1712,52 @@ library EngineBlox {
1678
1712
  address(this)
1679
1713
  ));
1680
1714
 
1715
+ TxParams memory tp = metaTx.txRecord.params;
1716
+ bytes32 txParamsStructHash = keccak256(abi.encode(
1717
+ TX_PARAMS_TYPE_HASH,
1718
+ tp.requester,
1719
+ tp.target,
1720
+ tp.value,
1721
+ tp.gasLimit,
1722
+ tp.operationType,
1723
+ tp.executionSelector,
1724
+ keccak256(tp.executionParams)
1725
+ ));
1726
+
1727
+ PaymentDetails memory payment = metaTx.txRecord.payment;
1728
+ bytes32 paymentStructHash = keccak256(abi.encode(
1729
+ PAYMENT_DETAILS_TYPE_HASH,
1730
+ payment.recipient,
1731
+ payment.nativeTokenAmount,
1732
+ payment.erc20TokenAddress,
1733
+ payment.erc20TokenAmount
1734
+ ));
1735
+
1736
+ bytes32 metaTxRecordStructHash = keccak256(abi.encode(
1737
+ META_TX_RECORD_TYPE_HASH,
1738
+ metaTx.txRecord.txId,
1739
+ txParamsStructHash,
1740
+ paymentStructHash
1741
+ ));
1742
+
1743
+ MetaTxParams memory mp = metaTx.params;
1744
+ bytes32 metaTxParamsStructHash = keccak256(abi.encode(
1745
+ META_TX_PARAMS_TYPE_HASH,
1746
+ mp.chainId,
1747
+ mp.nonce,
1748
+ mp.handlerContract,
1749
+ mp.handlerSelector,
1750
+ uint8(mp.action),
1751
+ mp.deadline,
1752
+ mp.maxGasPrice,
1753
+ mp.signer
1754
+ ));
1755
+
1681
1756
  bytes32 structHash = keccak256(abi.encode(
1682
- TYPE_HASH,
1683
- keccak256(abi.encode(
1684
- metaTx.txRecord.txId,
1685
- metaTx.txRecord.params.requester,
1686
- metaTx.txRecord.params.target,
1687
- metaTx.txRecord.params.value,
1688
- metaTx.txRecord.params.gasLimit,
1689
- metaTx.txRecord.params.operationType,
1690
- metaTx.txRecord.params.executionSelector,
1691
- keccak256(metaTx.txRecord.params.executionParams)
1692
- )),
1693
- metaTx.params.chainId,
1694
- metaTx.params.nonce,
1695
- metaTx.params.handlerContract,
1696
- metaTx.params.handlerSelector,
1697
- uint8(metaTx.params.action),
1698
- metaTx.params.deadline,
1699
- metaTx.params.maxGasPrice,
1700
- metaTx.params.signer
1757
+ META_TX_TYPE_HASH,
1758
+ metaTxRecordStructHash,
1759
+ metaTxParamsStructHash,
1760
+ keccak256(metaTx.data)
1701
1761
  ));
1702
1762
 
1703
1763
  return keccak256(abi.encodePacked(
@@ -1708,10 +1768,15 @@ library EngineBlox {
1708
1768
  }
1709
1769
 
1710
1770
  /**
1711
- * @dev Recovers the signer address from a message hash and signature.
1712
- * @param messageHash The hash of the message that was signed.
1713
- * @param signature The signature to recover the address from.
1714
- * @return The address of the signer.
1771
+ * @dev Recovers the signer from the EIP-712 digest and signature. Uses standard EIP-712 recovery (no message prefix).
1772
+ * Integrators must sign the digest returned by generateMessageHash as a raw hash—e.g.
1773
+ * account.sign({ hash: contractDigest }) or equivalent raw-hash signing API—with no
1774
+ * EIP-191/personal prefix, so signatures match this ecrecover(messageHash, v, r, s) verification.
1775
+ * Do NOT use personal_sign or generic eth_signTypedData_v4; the contract uses abi.encodePacked
1776
+ * for the domain version and a custom META_TX_TYPE_HASH, so those produce incompatible hashes.
1777
+ * @param messageHash The EIP-712 digest (keccak256("\x19\x01" || domainSeparator || structHash))
1778
+ * @param signature The signature (r, s, v)
1779
+ * @return The address of the signer
1715
1780
  */
1716
1781
  function recoverSigner(bytes32 messageHash, bytes memory signature) public pure returns (address) {
1717
1782
  SharedValidation.validateSignatureLength(signature);
@@ -1740,7 +1805,7 @@ library EngineBlox {
1740
1805
  // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}
1741
1806
  SharedValidation.validateSignatureParams(s, v);
1742
1807
 
1743
- address signer = ecrecover(messageHash.toEthSignedMessageHash(), v, r, s);
1808
+ address signer = ecrecover(messageHash, v, r, s);
1744
1809
  SharedValidation.validateRecoveredSigner(signer);
1745
1810
 
1746
1811
  return signer;
@@ -2109,14 +2174,72 @@ library EngineBlox {
2109
2174
  }
2110
2175
 
2111
2176
  /**
2112
- * @dev Validates that a wallet has permission for both execution selector and handler selector for a given action
2113
- * @param self The SecureOperationState to check
2114
- * @param wallet The wallet address to check permissions for
2115
- * @param executionSelector The execution function selector (underlying operation)
2116
- * @param handlerSelector The handler/calling function selector
2117
- * @param action The action to validate permissions for
2118
- * @notice This function consolidates the repeated dual permission check pattern to reduce contract size
2119
- * @notice Reverts with NoPermission if either permission check fails
2177
+ * @dev Validates that the meta-transaction txRecord matches the stored record for the given txId.
2178
+ * Ensures the signer's intent (as reflected in the stored tx from the request phase) is what
2179
+ * is approved or cancelled; override by meta-tx payload is not allowed for approve/cancel flows.
2180
+ * @param self The SecureOperationState containing the stored tx record.
2181
+ * @param txId The transaction ID.
2182
+ * @param metaTxRecord The TxRecord from the meta-transaction calldata.
2183
+ * @notice Reverts with MetaTxRecordMismatchStoredTx if any execution-affecting or permission-affecting field differs.
2184
+ */
2185
+ function _validateMetaTxMatchRecord(
2186
+ SecureOperationState storage self,
2187
+ uint256 txId,
2188
+ TxRecord memory metaTxRecord
2189
+ ) internal view {
2190
+ TxRecord storage stored = self.txRecords[txId];
2191
+ TxParams storage sp = stored.params;
2192
+ TxParams memory mp = metaTxRecord.params;
2193
+ if (
2194
+ sp.executionSelector != mp.executionSelector ||
2195
+ sp.target != mp.target ||
2196
+ sp.value != mp.value ||
2197
+ sp.requester != mp.requester ||
2198
+ sp.gasLimit != mp.gasLimit ||
2199
+ sp.operationType != mp.operationType ||
2200
+ keccak256(sp.executionParams) != keccak256(mp.executionParams) ||
2201
+ stored.releaseTime != metaTxRecord.releaseTime
2202
+ ) {
2203
+ revert SharedValidation.MetaTxRecordMismatchStoredTx(txId);
2204
+ }
2205
+ }
2206
+
2207
+ /**
2208
+ * @dev Validates that the meta-transaction payment matches the stored record for the given txId.
2209
+ * Ensures the signed payment (recipient, amounts, token) equals what will be executed.
2210
+ * @param self The SecureOperationState containing the stored tx record.
2211
+ * @param txId The transaction ID.
2212
+ * @param metaTxRecord The TxRecord from the meta-transaction calldata.
2213
+ * @notice Reverts with MetaTxPaymentMismatchStoredTx if any payment field differs from stored.
2214
+ */
2215
+ function _validateMetaTxPaymentMatchRecord(
2216
+ SecureOperationState storage self,
2217
+ uint256 txId,
2218
+ TxRecord memory metaTxRecord
2219
+ ) internal view {
2220
+ PaymentDetails storage storedPayment = self.txRecords[txId].payment;
2221
+ PaymentDetails memory metaPayment = metaTxRecord.payment;
2222
+ if (
2223
+ storedPayment.recipient != metaPayment.recipient ||
2224
+ storedPayment.nativeTokenAmount != metaPayment.nativeTokenAmount ||
2225
+ storedPayment.erc20TokenAddress != metaPayment.erc20TokenAddress ||
2226
+ storedPayment.erc20TokenAmount != metaPayment.erc20TokenAmount
2227
+ ) {
2228
+ revert SharedValidation.MetaTxPaymentMismatchStoredTx(txId);
2229
+ }
2230
+ }
2231
+
2232
+ /**
2233
+ * @dev Validates that a wallet has permission for both execution selector and handler selector for a given action.
2234
+ * @param self The SecureOperationState to check.
2235
+ * @param wallet The wallet address to check permissions for.
2236
+ * @param executionSelector The execution function selector (underlying operation).
2237
+ * @param handlerSelector The handler/calling function selector.
2238
+ * @param action The action to validate permissions for.
2239
+ * @notice This function consolidates the repeated dual permission check pattern to reduce contract size.
2240
+ * @notice Reverts with NoPermission if either permission check fails.
2241
+ * @notice Strict mode enforces that the handler's *schema-level* handlerForSelectors flow allows the execution selector;
2242
+ * it does not bind this relation to a specific role's FunctionPermission, which may further narrow pairings.
2120
2243
  */
2121
2244
  function _validateExecutionAndHandlerPermissions(
2122
2245
  SecureOperationState storage self,
@@ -2137,27 +2260,37 @@ library EngineBlox {
2137
2260
  if (!hasActionPermission(self, wallet, handlerSelector, action)) {
2138
2261
  revert SharedValidation.NoPermission(wallet);
2139
2262
  }
2263
+
2264
+ // In strict mode, enforce that the executionSelector is part of the handlerSelector's schema-level flow.
2265
+ // Handler schemas declare which execution selectors they are allowed to trigger globally; role permissions
2266
+ // can still narrow which selectors a given wallet may use via FunctionPermission.handlerForSelectors.
2267
+ FunctionSchema storage handlerSchema = self.functions[handlerSelector];
2268
+ if (handlerSchema.enforceHandlerRelations) {
2269
+ if (!_schemaHasHandlerSelector(handlerSchema, executionSelector)) {
2270
+ revert SharedValidation.HandlerForSelectorMismatch(
2271
+ executionSelector,
2272
+ handlerSelector
2273
+ );
2274
+ }
2275
+ }
2140
2276
  }
2141
2277
 
2142
2278
  /**
2143
- * @dev Validates that all handlerForSelectors are present in the schema's handlerForSelectors array
2279
+ * @dev Validates that all handlerForSelectors are present in the schema's handlerForSelectors array.
2280
+ * When schema.enforceHandlerRelations is false (flexible mode), validation is skipped and this function returns immediately.
2281
+ * When true (strict mode), every permission handlerForSelector must appear in the schema's handlerForSelectors.
2144
2282
  * @param self The SecureOperationState to validate against
2145
2283
  * @param functionSelector The function selector for which the permission is defined
2146
2284
  * @param handlerForSelectors The handlerForSelectors array from the permission to validate
2147
- * @notice Reverts with HandlerForSelectorMismatch if any handlerForSelector is not found in the schema's array
2148
- * @notice Special case: Execution function permissions should include functionSelector in handlerForSelectors (self-reference)
2285
+ * @notice Reverts with HandlerForSelectorMismatch if any handlerForSelector is not found in the schema's array (strict mode only).
2286
+ * @notice Special case: Execution function permissions should include functionSelector in handlerForSelectors (self-reference).
2149
2287
  */
2150
2288
  function _validateHandlerForSelectors(
2151
2289
  SecureOperationState storage self,
2152
2290
  bytes4 functionSelector,
2153
2291
  bytes4[] memory handlerForSelectors
2154
2292
  ) internal view {
2155
- bytes32 functionSelectorHash = bytes32(functionSelector);
2156
-
2157
- // Ensure the function schema exists
2158
- if (!self.supportedFunctionsSet.contains(functionSelectorHash)) {
2159
- revert SharedValidation.ResourceNotFound(functionSelectorHash);
2160
- }
2293
+ _validateFunctionSchemaExists(self, functionSelector);
2161
2294
 
2162
2295
  FunctionSchema storage schema = self.functions[functionSelector];
2163
2296
 
@@ -2170,14 +2303,7 @@ library EngineBlox {
2170
2303
  for (uint256 j = 0; j < handlerForSelectors.length; j++) {
2171
2304
  bytes4 handlerForSelector = handlerForSelectors[j];
2172
2305
 
2173
- bool found = false;
2174
- for (uint256 i = 0; i < schema.handlerForSelectors.length; i++) {
2175
- if (schema.handlerForSelectors[i] == handlerForSelector) {
2176
- found = true;
2177
- break;
2178
- }
2179
- }
2180
- if (!found) {
2306
+ if (!_schemaHasHandlerSelector(schema, handlerForSelector)) {
2181
2307
  revert SharedValidation.HandlerForSelectorMismatch(
2182
2308
  bytes4(0), // Cannot return array, use 0 as placeholder
2183
2309
  handlerForSelector
@@ -2186,6 +2312,24 @@ library EngineBlox {
2186
2312
  }
2187
2313
  }
2188
2314
 
2315
+ /**
2316
+ * @dev Checks whether a given handler selector is present in a function schema's handlerForSelectors array.
2317
+ * @param schema The function schema to inspect.
2318
+ * @param handlerSelector The handler selector to search for.
2319
+ * @return True if the handler selector is present, false otherwise.
2320
+ */
2321
+ function _schemaHasHandlerSelector(
2322
+ FunctionSchema storage schema,
2323
+ bytes4 handlerSelector
2324
+ ) internal view returns (bool) {
2325
+ for (uint256 i = 0; i < schema.handlerForSelectors.length; i++) {
2326
+ if (schema.handlerForSelectors[i] == handlerSelector) {
2327
+ return true;
2328
+ }
2329
+ }
2330
+ return false;
2331
+ }
2332
+
2189
2333
  /**
2190
2334
  * @dev Validates meta-transaction permissions for a function permission
2191
2335
  * @param self The secure operation state
@@ -73,6 +73,8 @@ library SharedValidation {
73
73
  error InvalidVValue(uint8 v);
74
74
  error ECDSAInvalidSignature(address recoveredSigner);
75
75
  error GasPriceExceedsMax(uint256 currentGasPrice, uint256 maxGasPrice);
76
+ error MetaTxRecordMismatchStoredTx(uint256 txId);
77
+ error MetaTxPaymentMismatchStoredTx(uint256 txId);
76
78
 
77
79
  // Consolidated resource errors
78
80
  error ResourceNotFound(bytes32 resourceId);
@@ -405,12 +407,12 @@ library SharedValidation {
405
407
  // ============ UTILITY FUNCTIONS ============
406
408
 
407
409
  /**
408
- * @dev Validates that the first value is less than the second value
409
- * @param from The first value (should be less than 'to')
410
- * @param to The second value (should be greater than 'from')
410
+ * @dev Validates that the first value is not greater than the second (allows inclusive range: from <= to)
411
+ * @param from The first value (must be <= 'to' for a valid range)
412
+ * @param to The second value (must be >= 'from')
411
413
  */
412
414
  function validateLessThan(uint256 from, uint256 to) internal pure {
413
- if (from >= to) revert InvalidRange(from, to);
415
+ if (from > to) revert InvalidRange(from, to);
414
416
  }
415
417
 
416
418
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloxchain/contracts",
3
- "version": "1.0.0-alpha.16",
3
+ "version": "1.0.0-alpha.17",
4
4
  "description": "Library engine for building enterprise grade decentralized permissioned applications",
5
5
  "files": [
6
6
  "core",