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

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
  {
@@ -667,38 +689,12 @@
667
689
  },
668
690
  {
669
691
  "inputs": [],
670
- "name": "VERSION_MAJOR",
671
- "outputs": [
672
- {
673
- "internalType": "uint8",
674
- "name": "",
675
- "type": "uint8"
676
- }
677
- ],
678
- "stateMutability": "view",
679
- "type": "function"
680
- },
681
- {
682
- "inputs": [],
683
- "name": "VERSION_MINOR",
692
+ "name": "VERSION",
684
693
  "outputs": [
685
694
  {
686
- "internalType": "uint8",
695
+ "internalType": "string",
687
696
  "name": "",
688
- "type": "uint8"
689
- }
690
- ],
691
- "stateMutability": "view",
692
- "type": "function"
693
- },
694
- {
695
- "inputs": [],
696
- "name": "VERSION_PATCH",
697
- "outputs": [
698
- {
699
- "internalType": "uint8",
700
- "name": "",
701
- "type": "uint8"
697
+ "type": "string"
702
698
  }
703
699
  ],
704
700
  "stateMutability": "view",
@@ -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
@@ -33,9 +32,7 @@ import "./interfaces/IEventForwarder.sol";
33
32
  library EngineBlox {
34
33
  // ============ VERSION INFORMATION ============
35
34
  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;
35
+ string public constant VERSION = "1.0.0";
39
36
 
40
37
  // ============ SYSTEM SAFETY LIMITS ============
41
38
  // These constants define the safety range limits for system operations
@@ -53,7 +50,6 @@ library EngineBlox {
53
50
  /// @dev Maximum total number of functions allowed in the system (prevents gas exhaustion in function operations)
54
51
  uint256 public constant MAX_FUNCTIONS = 2000;
55
52
 
56
- using MessageHashUtils for bytes32;
57
53
  using SharedValidation for *;
58
54
  using EnumerableSet for EnumerableSet.UintSet;
59
55
  using EnumerableSet for EnumerableSet.Bytes32Set;
@@ -152,9 +148,11 @@ library EngineBlox {
152
148
  bytes32 operationType;
153
149
  string operationName;
154
150
  uint16 supportedActionsBitmap; // Bitmap for TxAction enum (9 bits max)
155
- bool enforceHandlerRelations; // When true, handlerForSelectors in permissions must match schema.handlerForSelectors (except self-reference)
151
+ /// @dev When true (strict mode): handlerForSelectors in role permissions must match this schema's handlerForSelectors at use time.
152
+ /// When false (flexible mode): no such check; forward references and unregistered selectors in handlerForSelectors are allowed at registration.
153
+ bool enforceHandlerRelations;
156
154
  bool isProtected;
157
- bytes4[] handlerForSelectors;
155
+ bytes4[] handlerForSelectors;
158
156
  }
159
157
 
160
158
  // ============ DEFINITION STRUCTS ============
@@ -205,8 +203,29 @@ library EngineBlox {
205
203
  bytes4 public constant NATIVE_TRANSFER_SELECTOR = bytes4(keccak256("__bloxchain_native_transfer__()"));
206
204
  bytes32 public constant NATIVE_TRANSFER_OPERATION = keccak256("NATIVE_TRANSFER");
207
205
 
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)");
206
+ // EIP-712 Type Hashes (selective meta-tx payload: MetaTxRecord = txId + params + payment only)
207
+ // These follow the canonical EIP-712 convention so that eth_signTypedData_v4 and equivalent
208
+ // wallet typed-data signers can reproduce the same hashes when given matching type definitions.
209
+ //
210
+ // Canonical primary type string for MetaTransaction (primary type + all referenced types,
211
+ // appended in alphabetical order by type name
212
+ bytes32 private constant META_TX_TYPE_HASH = keccak256(
213
+ "MetaTransaction(MetaTxRecord txRecord,MetaTxParams params,bytes data)"
214
+ "MetaTxParams(uint256 chainId,uint256 nonce,address handlerContract,bytes4 handlerSelector,uint8 action,uint256 deadline,uint256 maxGasPrice,address signer)"
215
+ "MetaTxRecord(uint256 txId,TxParams params,PaymentDetails payment)"
216
+ "PaymentDetails(address recipient,uint256 nativeTokenAmount,address erc20TokenAddress,uint256 erc20TokenAmount)"
217
+ "TxParams(address requester,address target,uint256 value,uint256 gasLimit,bytes32 operationType,bytes4 executionSelector,bytes executionParams)"
218
+ );
219
+
220
+ // Canonical primary type string for MetaTxRecord (primary type + its referenced types).
221
+ bytes32 private constant META_TX_RECORD_TYPE_HASH = keccak256(
222
+ "MetaTxRecord(uint256 txId,TxParams params,PaymentDetails payment)"
223
+ "PaymentDetails(address recipient,uint256 nativeTokenAmount,address erc20TokenAddress,uint256 erc20TokenAmount)"
224
+ "TxParams(address requester,address target,uint256 value,uint256 gasLimit,bytes32 operationType,bytes4 executionSelector,bytes executionParams)"
225
+ );
226
+ bytes32 private constant TX_PARAMS_TYPE_HASH = keccak256("TxParams(address requester,address target,uint256 value,uint256 gasLimit,bytes32 operationType,bytes4 executionSelector,bytes executionParams)");
227
+ 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)");
228
+ bytes32 private constant PAYMENT_DETAILS_TYPE_HASH = keccak256("PaymentDetails(address recipient,uint256 nativeTokenAmount,address erc20TokenAddress,uint256 erc20TokenAmount)");
210
229
  bytes32 private constant DOMAIN_SEPARATOR_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
211
230
 
212
231
 
@@ -483,6 +502,8 @@ library EngineBlox {
483
502
  // Validate both execution and handler selector permissions
484
503
  _validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_CANCEL);
485
504
  _validateTxStatus(self, txId, TxStatus.PENDING);
505
+ _validateMetaTxMatchRecord(self, txId, metaTx.txRecord);
506
+ _validateMetaTxPaymentMatchRecord(self, txId, metaTx.txRecord);
486
507
  if (!verifySignature(self, metaTx)) revert SharedValidation.InvalidSignature(metaTx.signature);
487
508
 
488
509
  incrementSignerNonce(self, metaTx.params.signer);
@@ -513,10 +534,16 @@ library EngineBlox {
513
534
  * @return The updated TxRecord.
514
535
  * @notice This function skips permission validation and should only be called from functions
515
536
  * that have already validated permissions.
537
+ * @custom:security TIMELOCK: The releaseTime (timelock) is intentionally NOT enforced on this path.
538
+ * This is by design: the meta-tx workflow allows authorized signers to approve execution
539
+ * without waiting for releaseTime, providing a hybrid synergy between timelock workflows
540
+ * (direct path enforces releaseTime) and meta-tx workflows (delegated, time-flexible approval).
516
541
  */
517
542
  function _txApprovalWithMetaTx(SecureOperationState storage self, MetaTransaction memory metaTx) private returns (TxRecord memory) {
518
543
  uint256 txId = metaTx.txRecord.txId;
519
544
  _validateTxStatus(self, txId, TxStatus.PENDING);
545
+ _validateMetaTxMatchRecord(self, txId, metaTx.txRecord);
546
+ _validateMetaTxPaymentMatchRecord(self, txId, metaTx.txRecord);
520
547
  if (!verifySignature(self, metaTx)) revert SharedValidation.InvalidSignature(metaTx.signature);
521
548
 
522
549
  incrementSignerNonce(self, metaTx.params.signer);
@@ -838,6 +865,9 @@ library EngineBlox {
838
865
  * @param self The SecureOperationState to modify.
839
866
  * @param roleHash The hash of the role to remove.
840
867
  * @notice Security: Cannot remove protected roles to maintain system integrity.
868
+ * @custom:security PROTECTED-ROLE POLICY: This library enforces the protected-role check for
869
+ * REMOVE_ROLE. RuntimeRBAC does not duplicate this check; defense is in layers. The
870
+ * only component authorized to modify system wallets (protected roles) is SecureOwnable.
841
871
  */
842
872
  function removeRole(
843
873
  SecureOperationState storage self,
@@ -868,19 +898,27 @@ library EngineBlox {
868
898
  self.walletRoles[wallets[i]].remove(roleHash);
869
899
  }
870
900
 
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];
901
+ // Clear the role's authorizedWallets set so storage is clean if role is recreated with same name
902
+ for (uint256 i = 0; i < walletCount; i++) {
903
+ roleData.authorizedWallets.remove(wallets[i]);
904
+ }
905
+
906
+ // Clear function permissions and functionSelectorsSet (same reason: no stale data on role recreation)
907
+ uint256 selectorCount = roleData.functionSelectorsSet.length();
908
+ bytes32[] memory selectors = new bytes32[](selectorCount);
909
+ for (uint256 i = 0; i < selectorCount; i++) {
910
+ selectors[i] = roleData.functionSelectorsSet.at(i);
911
+ }
912
+ for (uint256 i = 0; i < selectorCount; i++) {
913
+ roleData.functionSelectorsSet.remove(selectors[i]);
914
+ delete roleData.functionPermissions[bytes4(selectors[i])];
915
+ }
916
+
917
+ // Delete role and remove from supported set (cleanup above ensures no stale RBAC data)
918
+ delete self.roles[roleHash];
881
919
  if (!self.supportedRolesSet.remove(roleHash)) {
882
920
  revert SharedValidation.ResourceNotFound(roleHash);
883
- }
921
+ }
884
922
  }
885
923
 
886
924
  /**
@@ -1120,9 +1158,12 @@ library EngineBlox {
1120
1158
  * @param functionSelector Hash identifier for the function.
1121
1159
  * @param operationName The name of the operation type.
1122
1160
  * @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).
1161
+ * @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
1162
  * @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
1163
+ * @param handlerForSelectors Non-empty array required - execution selectors must contain self-reference, handler selectors must point to execution selectors.
1164
+ * @custom:security OPERATIONAL MODES: We do not require handlerForSelectors[i] to be in supportedFunctionsSet at registration.
1165
+ * - Strict mode (enforceHandlerRelations == true): at use time (_validateHandlerForSelectors) we require role permissions' handlerForSelectors to match this schema's handlerForSelectors; registration order is flexible.
1166
+ * - Flexible mode (enforceHandlerRelations == false): validation is skipped; forward references and unregistered selectors are allowed by design. Callers select the mode per schema.
1126
1167
  */
1127
1168
  function registerFunction(
1128
1169
  SecureOperationState storage self,
@@ -1144,12 +1185,10 @@ library EngineBlox {
1144
1185
  // Derive operation type from operation name
1145
1186
  bytes32 derivedOperationType = keccak256(bytes(operationName));
1146
1187
 
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.
1188
+ // Validate handlerForSelectors: non-empty and all selectors are non-zero.
1189
+ // We do NOT require handlerForSelectors[i] to be in supportedFunctionsSet here.
1190
+ // Operational mode is controlled by enforceHandlerRelations: strict mode validates at use time;
1191
+ // flexible mode allows forward references and unregistered selectors by design. See @custom:security OPERATIONAL MODES above.
1153
1192
  if (handlerForSelectors.length == 0) {
1154
1193
  revert SharedValidation.OperationFailed();
1155
1194
  }
@@ -1665,39 +1704,84 @@ library EngineBlox {
1665
1704
  }
1666
1705
 
1667
1706
  /**
1668
- * @dev Generates a message hash for the specified meta-transaction following EIP-712
1707
+ * @dev Generates the EIP-712 message hash for the meta-transaction.
1708
+ * Uses selective MetaTxRecord (txId, params, payment only) with standard EIP-712 type hashes
1709
+ * so that eth_signTypedData_v4 (and equivalent) can reproduce the same digest when given
1710
+ * matching domain + types:
1711
+ *
1712
+ * - primaryType: MetaTransaction
1713
+ * - domain: { name: "Bloxchain", version: "1.0.0", chainId, verifyingContract }
1714
+ * - types: MetaTransaction, MetaTxRecord, TxParams, MetaTxParams, PaymentDetails
1715
+ *
1716
+ * Integrators MAY:
1717
+ * - use typed-data signing (eth_signTypedData_v4 / signTypedData) with the above domain/types, or
1718
+ * - sign the resulting digest as a raw hash (e.g. account.sign({ hash: contractDigest })).
1719
+ *
1720
+ * In all cases, on-chain verification uses recoverSigner(messageHash, signature) which applies
1721
+ * ecrecover(messageHash, v, r, s) with no personal_sign / EIP-191 prefix.
1722
+ *
1723
+ * The resulting digest is also written into the `message` field of helper-built `MetaTransaction`
1724
+ * structs so integrators can use it directly without recomputing the hash client-side.
1669
1725
  * @param metaTx The meta-transaction to generate the hash for
1670
- * @return The generated message hash
1726
+ * @return The EIP-712 digest (no prefix; use standard recovery)
1671
1727
  */
1672
1728
  function generateMessageHash(MetaTransaction memory metaTx) private view returns (bytes32) {
1673
- bytes32 domainSeparator = keccak256(abi.encode(
1674
- DOMAIN_SEPARATOR_TYPE_HASH,
1675
- PROTOCOL_NAME_HASH,
1676
- keccak256(abi.encodePacked(VERSION_MAJOR, ".", VERSION_MINOR, ".", VERSION_PATCH)),
1677
- block.chainid,
1678
- address(this)
1729
+ bytes32 domainSeparator = keccak256(
1730
+ abi.encode(
1731
+ DOMAIN_SEPARATOR_TYPE_HASH,
1732
+ PROTOCOL_NAME_HASH,
1733
+ keccak256(bytes(VERSION)),
1734
+ block.chainid,
1735
+ address(this)
1736
+ )
1737
+ );
1738
+
1739
+ TxParams memory tp = metaTx.txRecord.params;
1740
+ bytes32 txParamsStructHash = keccak256(abi.encode(
1741
+ TX_PARAMS_TYPE_HASH,
1742
+ tp.requester,
1743
+ tp.target,
1744
+ tp.value,
1745
+ tp.gasLimit,
1746
+ tp.operationType,
1747
+ tp.executionSelector,
1748
+ keccak256(tp.executionParams)
1749
+ ));
1750
+
1751
+ PaymentDetails memory payment = metaTx.txRecord.payment;
1752
+ bytes32 paymentStructHash = keccak256(abi.encode(
1753
+ PAYMENT_DETAILS_TYPE_HASH,
1754
+ payment.recipient,
1755
+ payment.nativeTokenAmount,
1756
+ payment.erc20TokenAddress,
1757
+ payment.erc20TokenAmount
1758
+ ));
1759
+
1760
+ bytes32 metaTxRecordStructHash = keccak256(abi.encode(
1761
+ META_TX_RECORD_TYPE_HASH,
1762
+ metaTx.txRecord.txId,
1763
+ txParamsStructHash,
1764
+ paymentStructHash
1765
+ ));
1766
+
1767
+ MetaTxParams memory mp = metaTx.params;
1768
+ bytes32 metaTxParamsStructHash = keccak256(abi.encode(
1769
+ META_TX_PARAMS_TYPE_HASH,
1770
+ mp.chainId,
1771
+ mp.nonce,
1772
+ mp.handlerContract,
1773
+ mp.handlerSelector,
1774
+ uint8(mp.action),
1775
+ mp.deadline,
1776
+ mp.maxGasPrice,
1777
+ mp.signer
1679
1778
  ));
1680
1779
 
1681
1780
  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
1781
+ META_TX_TYPE_HASH,
1782
+ metaTxRecordStructHash,
1783
+ metaTxParamsStructHash,
1784
+ keccak256(metaTx.data)
1701
1785
  ));
1702
1786
 
1703
1787
  return keccak256(abi.encodePacked(
@@ -1708,10 +1792,24 @@ library EngineBlox {
1708
1792
  }
1709
1793
 
1710
1794
  /**
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.
1795
+ * @dev Recovers the signer from the EIP-712 digest and signature. Uses standard EIP-712 recovery (no message prefix).
1796
+ *
1797
+ * Integrators have two equivalent options:
1798
+ * - Use typed-data signing (eth_signTypedData_v4 / signTypedData) with:
1799
+ * - primaryType: MetaTransaction
1800
+ * - domain: { name: "Bloxchain", version: "1.0.0", chainId, verifyingContract }
1801
+ * - types: MetaTransaction, MetaTxRecord, TxParams, MetaTxParams, PaymentDetails
1802
+ * In this case the wallet computes the same digest as generateMessageHash and signs it.
1803
+ * - Sign the digest returned by generateMessageHash as a raw hash—e.g.
1804
+ * account.sign({ hash: contractDigest }) or equivalent raw-hash signing API—with no
1805
+ * EIP-191/personal prefix.
1806
+ *
1807
+ * In all cases, this function applies ecrecover(messageHash, v, r, s) over the raw EIP-712 digest.
1808
+ * personal_sign / EIP-191-prefixed signatures remain incompatible.
1809
+ *
1810
+ * @param messageHash The EIP-712 digest (keccak256("\x19\x01" || domainSeparator || structHash))
1811
+ * @param signature The signature (r, s, v)
1812
+ * @return The address of the signer
1715
1813
  */
1716
1814
  function recoverSigner(bytes32 messageHash, bytes memory signature) public pure returns (address) {
1717
1815
  SharedValidation.validateSignatureLength(signature);
@@ -1740,7 +1838,7 @@ library EngineBlox {
1740
1838
  // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}
1741
1839
  SharedValidation.validateSignatureParams(s, v);
1742
1840
 
1743
- address signer = ecrecover(messageHash.toEthSignedMessageHash(), v, r, s);
1841
+ address signer = ecrecover(messageHash, v, r, s);
1744
1842
  SharedValidation.validateRecoveredSigner(signer);
1745
1843
 
1746
1844
  return signer;
@@ -2109,14 +2207,72 @@ library EngineBlox {
2109
2207
  }
2110
2208
 
2111
2209
  /**
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
2210
+ * @dev Validates that the meta-transaction txRecord matches the stored record for the given txId.
2211
+ * Ensures the signer's intent (as reflected in the stored tx from the request phase) is what
2212
+ * is approved or cancelled; override by meta-tx payload is not allowed for approve/cancel flows.
2213
+ * @param self The SecureOperationState containing the stored tx record.
2214
+ * @param txId The transaction ID.
2215
+ * @param metaTxRecord The TxRecord from the meta-transaction calldata.
2216
+ * @notice Reverts with MetaTxRecordMismatchStoredTx if any execution-affecting or permission-affecting field differs.
2217
+ */
2218
+ function _validateMetaTxMatchRecord(
2219
+ SecureOperationState storage self,
2220
+ uint256 txId,
2221
+ TxRecord memory metaTxRecord
2222
+ ) internal view {
2223
+ TxRecord storage stored = self.txRecords[txId];
2224
+ TxParams storage sp = stored.params;
2225
+ TxParams memory mp = metaTxRecord.params;
2226
+ if (
2227
+ sp.executionSelector != mp.executionSelector ||
2228
+ sp.target != mp.target ||
2229
+ sp.value != mp.value ||
2230
+ sp.requester != mp.requester ||
2231
+ sp.gasLimit != mp.gasLimit ||
2232
+ sp.operationType != mp.operationType ||
2233
+ keccak256(sp.executionParams) != keccak256(mp.executionParams) ||
2234
+ stored.releaseTime != metaTxRecord.releaseTime
2235
+ ) {
2236
+ revert SharedValidation.MetaTxRecordMismatchStoredTx(txId);
2237
+ }
2238
+ }
2239
+
2240
+ /**
2241
+ * @dev Validates that the meta-transaction payment matches the stored record for the given txId.
2242
+ * Ensures the signed payment (recipient, amounts, token) equals what will be executed.
2243
+ * @param self The SecureOperationState containing the stored tx record.
2244
+ * @param txId The transaction ID.
2245
+ * @param metaTxRecord The TxRecord from the meta-transaction calldata.
2246
+ * @notice Reverts with MetaTxPaymentMismatchStoredTx if any payment field differs from stored.
2247
+ */
2248
+ function _validateMetaTxPaymentMatchRecord(
2249
+ SecureOperationState storage self,
2250
+ uint256 txId,
2251
+ TxRecord memory metaTxRecord
2252
+ ) internal view {
2253
+ PaymentDetails storage storedPayment = self.txRecords[txId].payment;
2254
+ PaymentDetails memory metaPayment = metaTxRecord.payment;
2255
+ if (
2256
+ storedPayment.recipient != metaPayment.recipient ||
2257
+ storedPayment.nativeTokenAmount != metaPayment.nativeTokenAmount ||
2258
+ storedPayment.erc20TokenAddress != metaPayment.erc20TokenAddress ||
2259
+ storedPayment.erc20TokenAmount != metaPayment.erc20TokenAmount
2260
+ ) {
2261
+ revert SharedValidation.MetaTxPaymentMismatchStoredTx(txId);
2262
+ }
2263
+ }
2264
+
2265
+ /**
2266
+ * @dev Validates that a wallet has permission for both execution selector and handler selector for a given action.
2267
+ * @param self The SecureOperationState to check.
2268
+ * @param wallet The wallet address to check permissions for.
2269
+ * @param executionSelector The execution function selector (underlying operation).
2270
+ * @param handlerSelector The handler/calling function selector.
2271
+ * @param action The action to validate permissions for.
2272
+ * @notice This function consolidates the repeated dual permission check pattern to reduce contract size.
2273
+ * @notice Reverts with NoPermission if either permission check fails.
2274
+ * @notice Strict mode enforces that the handler's *schema-level* handlerForSelectors flow allows the execution selector;
2275
+ * it does not bind this relation to a specific role's FunctionPermission, which may further narrow pairings.
2120
2276
  */
2121
2277
  function _validateExecutionAndHandlerPermissions(
2122
2278
  SecureOperationState storage self,
@@ -2137,27 +2293,37 @@ library EngineBlox {
2137
2293
  if (!hasActionPermission(self, wallet, handlerSelector, action)) {
2138
2294
  revert SharedValidation.NoPermission(wallet);
2139
2295
  }
2296
+
2297
+ // In strict mode, enforce that the executionSelector is part of the handlerSelector's schema-level flow.
2298
+ // Handler schemas declare which execution selectors they are allowed to trigger globally; role permissions
2299
+ // can still narrow which selectors a given wallet may use via FunctionPermission.handlerForSelectors.
2300
+ FunctionSchema storage handlerSchema = self.functions[handlerSelector];
2301
+ if (handlerSchema.enforceHandlerRelations) {
2302
+ if (!_schemaHasHandlerSelector(handlerSchema, executionSelector)) {
2303
+ revert SharedValidation.HandlerForSelectorMismatch(
2304
+ executionSelector,
2305
+ handlerSelector
2306
+ );
2307
+ }
2308
+ }
2140
2309
  }
2141
2310
 
2142
2311
  /**
2143
- * @dev Validates that all handlerForSelectors are present in the schema's handlerForSelectors array
2312
+ * @dev Validates that all handlerForSelectors are present in the schema's handlerForSelectors array.
2313
+ * When schema.enforceHandlerRelations is false (flexible mode), validation is skipped and this function returns immediately.
2314
+ * When true (strict mode), every permission handlerForSelector must appear in the schema's handlerForSelectors.
2144
2315
  * @param self The SecureOperationState to validate against
2145
2316
  * @param functionSelector The function selector for which the permission is defined
2146
2317
  * @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)
2318
+ * @notice Reverts with HandlerForSelectorMismatch if any handlerForSelector is not found in the schema's array (strict mode only).
2319
+ * @notice Special case: Execution function permissions should include functionSelector in handlerForSelectors (self-reference).
2149
2320
  */
2150
2321
  function _validateHandlerForSelectors(
2151
2322
  SecureOperationState storage self,
2152
2323
  bytes4 functionSelector,
2153
2324
  bytes4[] memory handlerForSelectors
2154
2325
  ) 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
- }
2326
+ _validateFunctionSchemaExists(self, functionSelector);
2161
2327
 
2162
2328
  FunctionSchema storage schema = self.functions[functionSelector];
2163
2329
 
@@ -2170,14 +2336,7 @@ library EngineBlox {
2170
2336
  for (uint256 j = 0; j < handlerForSelectors.length; j++) {
2171
2337
  bytes4 handlerForSelector = handlerForSelectors[j];
2172
2338
 
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) {
2339
+ if (!_schemaHasHandlerSelector(schema, handlerForSelector)) {
2181
2340
  revert SharedValidation.HandlerForSelectorMismatch(
2182
2341
  bytes4(0), // Cannot return array, use 0 as placeholder
2183
2342
  handlerForSelector
@@ -2186,6 +2345,24 @@ library EngineBlox {
2186
2345
  }
2187
2346
  }
2188
2347
 
2348
+ /**
2349
+ * @dev Checks whether a given handler selector is present in a function schema's handlerForSelectors array.
2350
+ * @param schema The function schema to inspect.
2351
+ * @param handlerSelector The handler selector to search for.
2352
+ * @return True if the handler selector is present, false otherwise.
2353
+ */
2354
+ function _schemaHasHandlerSelector(
2355
+ FunctionSchema storage schema,
2356
+ bytes4 handlerSelector
2357
+ ) internal view returns (bool) {
2358
+ for (uint256 i = 0; i < schema.handlerForSelectors.length; i++) {
2359
+ if (schema.handlerForSelectors[i] == handlerSelector) {
2360
+ return true;
2361
+ }
2362
+ }
2363
+ return false;
2364
+ }
2365
+
2189
2366
  /**
2190
2367
  * @dev Validates meta-transaction permissions for a function permission
2191
2368
  * @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.18",
4
4
  "description": "Library engine for building enterprise grade decentralized permissioned applications",
5
5
  "files": [
6
6
  "core",