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