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