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

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.
@@ -51,6 +51,19 @@
51
51
  "stateMutability": "view",
52
52
  "type": "function"
53
53
  },
54
+ {
55
+ "inputs": [],
56
+ "name": "CONTROLLER_CONFIG_OPERATION",
57
+ "outputs": [
58
+ {
59
+ "internalType": "bytes32",
60
+ "name": "",
61
+ "type": "bytes32"
62
+ }
63
+ ],
64
+ "stateMutability": "view",
65
+ "type": "function"
66
+ },
54
67
  {
55
68
  "inputs": [],
56
69
  "name": "CONTROLLER_OPERATION",
@@ -134,7 +134,7 @@ abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
134
134
  function _executeRoleConfigBatch(IRuntimeRBAC.RoleConfigAction[] calldata actions) internal {
135
135
  _validateBatchSize(actions.length);
136
136
 
137
- for (uint256 i = 0; i < actions.length; i++) {
137
+ for (uint256 i = 0; i < actions.length; ++i) {
138
138
  IRuntimeRBAC.RoleConfigAction calldata action = actions[i];
139
139
 
140
140
  if (action.actionType == IRuntimeRBAC.RoleConfigActionType.CREATE_ROLE) {
@@ -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 6 GuardController public execution functions
19
+ * - Registers all 9 GuardController public execution functions
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
@@ -33,6 +33,8 @@ library GuardControllerDefinitions {
33
33
 
34
34
  // Operation Type Constants
35
35
  bytes32 public constant CONTROLLER_OPERATION = keccak256("CONTROLLER_OPERATION");
36
+ // Guard config batch only (whitelist / register-unregister function); distinct execution operation type bitmap.
37
+ bytes32 public constant CONTROLLER_CONFIG_OPERATION = keccak256("CONTROLLER_CONFIG_OPERATION");
36
38
 
37
39
  // Function Selector Constants
38
40
  // GuardController: executeWithTimeLock(address,uint256,bytes4,bytes,uint256,bytes32)
@@ -86,7 +88,7 @@ library GuardControllerDefinitions {
86
88
  *
87
89
  * Function schemas define:
88
90
  * - GuardController public execution functions
89
- * - What operation types they belong to (CONTROLLER_OPERATION)
91
+ * - What operation types they belong to (CONTROLLER_OPERATION vs CONTROLLER_CONFIG_OPERATION)
90
92
  * - What actions are supported (time-delay request/approve/cancel, meta-tx approve/cancel/request-and-approve)
91
93
  * - Whether they are protected
92
94
  *
@@ -96,7 +98,7 @@ library GuardControllerDefinitions {
96
98
  * - Role permissions are defined in getRolePermissions() matching EngineBloxDefinitions pattern
97
99
  */
98
100
  function getFunctionSchemas() public pure returns (EngineBlox.FunctionSchema[] memory) {
99
- EngineBlox.FunctionSchema[] memory schemas = new EngineBlox.FunctionSchema[](8);
101
+ EngineBlox.FunctionSchema[] memory schemas = new EngineBlox.FunctionSchema[](9);
100
102
 
101
103
  // ============ TIME-DELAY WORKFLOW ACTIONS ============
102
104
  // Request action for executeWithTimeLock
@@ -144,6 +146,9 @@ library GuardControllerDefinitions {
144
146
  requestAndApproveExecutionHandlerForSelectors[0] = REQUEST_AND_APPROVE_EXECUTION_SELECTOR;
145
147
  bytes4[] memory guardConfigBatchExecuteHandlerForSelectors = new bytes4[](1);
146
148
  guardConfigBatchExecuteHandlerForSelectors[0] = GUARD_CONFIG_BATCH_EXECUTE_SELECTOR;
149
+
150
+ bytes4[] memory executeWithPaymentHandlerForSelectors = new bytes4[](1);
151
+ executeWithPaymentHandlerForSelectors[0] = EXECUTE_WITH_PAYMENT_SELECTOR;
147
152
 
148
153
  // Handler selectors point to execution selectors
149
154
  bytes4[] memory guardConfigHandlerForSelectors = new bytes4[](1);
@@ -225,8 +230,8 @@ library GuardControllerDefinitions {
225
230
  schemas[6] = EngineBlox.FunctionSchema({
226
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))",
227
232
  functionSelector: GUARD_CONFIG_BATCH_META_SELECTOR,
228
- operationType: CONTROLLER_OPERATION,
229
- operationName: "CONTROLLER_OPERATION",
233
+ operationType: CONTROLLER_CONFIG_OPERATION,
234
+ operationName: "CONTROLLER_CONFIG_OPERATION",
230
235
  supportedActionsBitmap: EngineBlox.createBitmapFromActions(metaTxRequestApproveActions),
231
236
  enforceHandlerRelations: true,
232
237
  isProtected: true,
@@ -241,14 +246,27 @@ library GuardControllerDefinitions {
241
246
  schemas[7] = EngineBlox.FunctionSchema({
242
247
  functionSignature: "executeGuardConfigBatch((uint8,bytes)[])",
243
248
  functionSelector: GUARD_CONFIG_BATCH_EXECUTE_SELECTOR,
244
- operationType: CONTROLLER_OPERATION,
245
- operationName: "CONTROLLER_OPERATION",
249
+ operationType: CONTROLLER_CONFIG_OPERATION,
250
+ operationName: "CONTROLLER_CONFIG_OPERATION",
246
251
  supportedActionsBitmap: EngineBlox.createBitmapFromActions(guardConfigExecutionActions),
247
252
  enforceHandlerRelations: false,
248
253
  isProtected: true,
249
254
  handlerForSelectors: guardConfigBatchExecuteHandlerForSelectors
250
255
  });
251
256
 
257
+ // Schema 8: GuardController.executeWithPayment (same time-delay request action as executeWithTimeLock;
258
+ // OWNER_ROLE grant for this selector may be added manually if the flow is enabled)
259
+ schemas[8] = EngineBlox.FunctionSchema({
260
+ functionSignature: "executeWithPayment(address,uint256,bytes4,bytes,uint256,bytes32,(address,uint256,address,uint256))",
261
+ functionSelector: EXECUTE_WITH_PAYMENT_SELECTOR,
262
+ operationType: CONTROLLER_OPERATION,
263
+ operationName: "CONTROLLER_OPERATION",
264
+ supportedActionsBitmap: EngineBlox.createBitmapFromActions(timeDelayRequestActions),
265
+ enforceHandlerRelations: false,
266
+ isProtected: true,
267
+ handlerForSelectors: executeWithPaymentHandlerForSelectors
268
+ });
269
+
252
270
  return schemas;
253
271
  }
254
272
 
@@ -376,6 +376,16 @@ library EngineBlox {
376
376
  // Validate both execution and handler selector permissions (same as txRequest)
377
377
  _validateExecutionAndHandlerPermissions(self, msg.sender, executionSelector, handlerSelector, TxAction.EXECUTE_TIME_DELAY_REQUEST);
378
378
 
379
+ // Request-time validation for attached payment details.
380
+ // This prevents creating persistent PENDING records that later fail during
381
+ // `executeAttachedPayment` due to missing/zero payment fields.
382
+ if (paymentDetails.nativeTokenAmount > 0 || paymentDetails.erc20TokenAmount > 0) {
383
+ SharedValidation.validateNotZeroAddress(paymentDetails.recipient);
384
+ }
385
+ if (paymentDetails.erc20TokenAmount > 0) {
386
+ SharedValidation.validateNotZeroAddress(paymentDetails.erc20TokenAddress);
387
+ }
388
+
379
389
  return _txRequest(
380
390
  self,
381
391
  requester,
@@ -574,6 +584,16 @@ library EngineBlox {
574
584
  // Validate both execution and handler selector permissions
575
585
  _validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_REQUEST_AND_APPROVE);
576
586
 
587
+ // Request-time validation for attached payment details.
588
+ // `requestAndApprove` creates the request and executes via the same meta-tx flow,
589
+ // so we validate here to avoid persisting bad PENDING records.
590
+ if (metaTx.txRecord.payment.nativeTokenAmount > 0 || metaTx.txRecord.payment.erc20TokenAmount > 0) {
591
+ SharedValidation.validateNotZeroAddress(metaTx.txRecord.payment.recipient);
592
+ }
593
+ if (metaTx.txRecord.payment.erc20TokenAmount > 0) {
594
+ SharedValidation.validateNotZeroAddress(metaTx.txRecord.payment.erc20TokenAddress);
595
+ }
596
+
577
597
  TxRecord memory txRecord = _txRequest(
578
598
  self,
579
599
  metaTx.txRecord.params.requester,
@@ -2,8 +2,11 @@
2
2
  pragma solidity 0.8.34;
3
3
 
4
4
  import "../execution/GuardController.sol";
5
+ import "../execution/interface/IGuardController.sol";
5
6
  import "../access/RuntimeRBAC.sol";
7
+ import "../access/interface/IRuntimeRBAC.sol";
6
8
  import "../security/SecureOwnable.sol";
9
+ import "../security/interface/ISecureOwnable.sol";
7
10
  import "../lib/utils/SharedValidation.sol";
8
11
 
9
12
  /**
@@ -51,9 +54,15 @@ abstract contract Account is GuardController, RuntimeRBAC, SecureOwnable {
51
54
 
52
55
  /**
53
56
  * @dev See {IERC165-supportsInterface}.
57
+ * @notice GuardController, RuntimeRBAC, and SecureOwnable each extend BaseStateMachine directly; a single
58
+ * `super` chain only walks one branch. We OR the three component interface IDs here, then delegate
59
+ * once to `super` for IBaseStateMachine / ERC165 — avoids tripling BaseStateMachine+ERC165 work.
54
60
  */
55
61
  function supportsInterface(bytes4 interfaceId) public view virtual override(GuardController, RuntimeRBAC, SecureOwnable) returns (bool) {
56
- return GuardController.supportsInterface(interfaceId) || RuntimeRBAC.supportsInterface(interfaceId) || SecureOwnable.supportsInterface(interfaceId);
62
+ return interfaceId == type(IGuardController).interfaceId
63
+ || interfaceId == type(IRuntimeRBAC).interfaceId
64
+ || interfaceId == type(ISecureOwnable).interfaceId
65
+ || super.supportsInterface(interfaceId);
57
66
  }
58
67
 
59
68
  /**
@@ -1,394 +1,424 @@
1
- // SPDX-License-Identifier: MPL-2.0
2
- pragma solidity 0.8.34;
3
-
4
- // Contracts imports
5
- import "../base/BaseStateMachine.sol";
6
- import "./lib/definitions/SecureOwnableDefinitions.sol";
7
- import "../lib/interfaces/IDefinition.sol";
8
- import "../lib/utils/SharedValidation.sol";
9
- import "./interface/ISecureOwnable.sol";
10
-
11
- /**
12
- * @title SecureOwnable
13
- * @dev Security-focused contract extending BaseStateMachine with ownership management
14
- *
15
- * SecureOwnable provides security-specific functionality built on top of the base state machine:
16
- * - Multi-role security model with Owner, Broadcaster, and Recovery roles
17
- * - Secure ownership transfer with time-locked operations
18
- * - Broadcaster and recovery address management
19
- * - Time-lock period configuration
20
- *
21
- * The contract implements four primary secure operation types:
22
- * 1. OWNERSHIP_TRANSFER - For securely transferring contract ownership
23
- * 2. BROADCASTER_UPDATE - For changing the broadcaster address
24
- * 3. RECOVERY_UPDATE - For updating the recovery address
25
- * 4. TIMELOCK_UPDATE - For modifying the time lock period
26
- *
27
- * Each operation follows a request -> approval workflow with appropriate time locks
28
- * and authorization checks. Operations can be cancelled within specific time windows.
29
- *
30
- * At most one ownership-transfer or broadcaster-update request may be pending at a time:
31
- * a pending request of either type blocks new requests until it is approved or cancelled.
32
- *
33
- * This contract focuses purely on security logic while leveraging the BaseStateMachine
34
- * for transaction management, meta-transactions, and state machine operations.
35
- */
36
- abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
37
- using SharedValidation for *;
38
-
39
- /// @dev True while any pending ownership transfer or broadcaster update request exists; blocks new requests until handled.
40
- bool private _hasOpenRequest;
41
-
42
- /**
43
- * @notice Initializer to initialize SecureOwnable state
44
- * @param initialOwner The initial owner address
45
- * @param broadcaster The broadcaster address
46
- * @param recovery The recovery address
47
- * @param timeLockPeriodSec The timelock period in seconds
48
- * @param eventForwarder The event forwarder address
49
- */
50
- function initialize(
51
- address initialOwner,
52
- address broadcaster,
53
- address recovery,
54
- uint256 timeLockPeriodSec,
55
- address eventForwarder
56
- ) public virtual onlyInitializing {
57
- _initializeBaseStateMachine(initialOwner, broadcaster, recovery, timeLockPeriodSec, eventForwarder);
58
-
59
- // Load SecureOwnable-specific definitions
60
- IDefinition.RolePermission memory secureOwnablePermissions = SecureOwnableDefinitions.getRolePermissions();
61
- _loadDefinitions(
62
- SecureOwnableDefinitions.getFunctionSchemas(),
63
- secureOwnablePermissions.roleHashes,
64
- secureOwnablePermissions.functionPermissions,
65
- true // Enforce all function schemas are protected
66
- );
67
- }
68
-
69
- // ============ INTERFACE SUPPORT ============
70
-
71
- /**
72
- * @dev See {IERC165-supportsInterface}.
73
- * @notice Adds ISecureOwnable interface ID for component detection
74
- */
75
- function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
76
- return interfaceId == type(ISecureOwnable).interfaceId || super.supportsInterface(interfaceId);
77
- }
78
-
79
- // Ownership Management
80
- /**
81
- * @dev Requests a transfer of ownership
82
- * @return txId The transaction ID (use getTransaction(txId) for full record)
83
- */
84
- function transferOwnershipRequest() public returns (uint256 txId) {
85
- SharedValidation.validateRecovery(getRecovery());
86
- _requireNoPendingRequest();
87
-
88
- EngineBlox.TxRecord memory txRecord = _requestTransaction(
89
- msg.sender,
90
- address(this),
91
- 0, // value
92
- 0, // no gas limit
93
- SecureOwnableDefinitions.OWNERSHIP_TRANSFER,
94
- SecureOwnableDefinitions.TRANSFER_OWNERSHIP_SELECTOR,
95
- abi.encode(getRecovery())
96
- );
97
-
98
- _hasOpenRequest = true;
99
- _logAddressPairEvent(owner(), getRecovery());
100
- return txRecord.txId;
101
- }
102
-
103
- /**
104
- * @dev Approves a pending ownership transfer transaction after the release time
105
- * @param txId The transaction ID
106
- * @return The transaction ID
107
- */
108
- function transferOwnershipDelayedApproval(uint256 txId) public returns (uint256) {
109
- SharedValidation.validateOwnerOrRecovery(owner(), getRecovery());
110
- return _completeApprove(_approveTransaction(txId));
111
- }
112
-
113
- /**
114
- * @dev Approves a pending ownership transfer transaction using a meta-transaction
115
- * @param metaTx The meta-transaction
116
- * @return The transaction ID
117
- */
118
- function transferOwnershipApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
119
- _validateBroadcasterAndOwnerSigner(metaTx);
120
- return _completeApprove(_approveTransactionWithMetaTx(metaTx));
121
- }
122
-
123
- /**
124
- * @dev Cancels a pending ownership transfer transaction
125
- * @param txId The transaction ID
126
- * @return The transaction ID
127
- */
128
- function transferOwnershipCancellation(uint256 txId) public returns (uint256) {
129
- SharedValidation.validateRecovery(getRecovery());
130
- return _completeCancel(_cancelTransaction(txId));
131
- }
132
-
133
- /**
134
- * @dev Cancels a pending ownership transfer transaction using a meta-transaction
135
- * @param metaTx The meta-transaction
136
- * @return The transaction ID
137
- */
138
- function transferOwnershipCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
139
- _validateBroadcasterAndOwnerSigner(metaTx);
140
- return _completeCancel(_cancelTransactionWithMetaTx(metaTx));
141
- }
142
-
143
- // Broadcaster Management
144
- /**
145
- * @dev Requests an update to the broadcaster at a specific location (index).
146
- * @param newBroadcaster The new broadcaster address (zero address to revoke at location)
147
- * @param location The index in the broadcaster role's authorized wallets set
148
- * @return txId The transaction ID for the pending request (use getTransaction(txId) for full record)
149
- */
150
- function updateBroadcasterRequest(address newBroadcaster, uint256 location) public returns (uint256 txId) {
151
- SharedValidation.validateOwner(owner());
152
- _requireNoPendingRequest();
153
-
154
- // Get the current broadcaster at the specified location. zero address if no broadcaster at location.
155
- address currentBroadcaster = location < _getSecureState().roles[EngineBlox.BROADCASTER_ROLE].walletCount
156
- ? _getAuthorizedWalletAt(EngineBlox.BROADCASTER_ROLE, location)
157
- : address(0);
158
-
159
- EngineBlox.TxRecord memory txRecord = _requestTransaction(
160
- msg.sender,
161
- address(this),
162
- 0, // value
163
- 0, // gas limit
164
- SecureOwnableDefinitions.BROADCASTER_UPDATE,
165
- SecureOwnableDefinitions.UPDATE_BROADCASTER_SELECTOR,
166
- abi.encode(newBroadcaster, location)
167
- );
168
-
169
- _hasOpenRequest = true;
170
- _logAddressPairEvent(currentBroadcaster, newBroadcaster);
171
- return txRecord.txId;
172
- }
173
-
174
- /**
175
- * @dev Approves a pending broadcaster update transaction after the release time
176
- * @param txId The transaction ID
177
- * @return The transaction ID
178
- */
179
- function updateBroadcasterDelayedApproval(uint256 txId) public returns (uint256) {
180
- SharedValidation.validateOwner(owner());
181
- return _completeApprove(_approveTransaction(txId));
182
- }
183
-
184
- /**
185
- * @dev Approves a pending broadcaster update transaction using a meta-transaction
186
- * @param metaTx The meta-transaction
187
- * @return The transaction ID
188
- */
189
- function updateBroadcasterApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
190
- _validateBroadcasterAndOwnerSigner(metaTx);
191
- return _completeApprove(_approveTransactionWithMetaTx(metaTx));
192
- }
193
-
194
- /**
195
- * @dev Cancels a pending broadcaster update transaction
196
- * @param txId The transaction ID
197
- * @return The transaction ID
198
- */
199
- function updateBroadcasterCancellation(uint256 txId) public returns (uint256) {
200
- SharedValidation.validateOwner(owner());
201
- return _completeCancel(_cancelTransaction(txId));
202
- }
203
-
204
- /**
205
- * @dev Cancels a pending broadcaster update transaction using a meta-transaction
206
- * @param metaTx The meta-transaction
207
- * @return The transaction ID
208
- */
209
- function updateBroadcasterCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
210
- _validateBroadcasterAndOwnerSigner(metaTx);
211
- return _completeCancel(_cancelTransactionWithMetaTx(metaTx));
212
- }
213
-
214
- // Recovery Management
215
-
216
- /**
217
- * @dev Requests and approves a recovery address update using a meta-transaction
218
- * @param metaTx The meta-transaction
219
- * @return The transaction ID
220
- */
221
- function updateRecoveryRequestAndApprove(
222
- EngineBlox.MetaTransaction memory metaTx
223
- ) public returns (uint256) {
224
- _validateBroadcasterAndOwnerSigner(metaTx);
225
- EngineBlox.TxRecord memory txRecord = _requestAndApproveTransaction(metaTx);
226
- return txRecord.txId;
227
- }
228
-
229
- // TimeLock Management
230
-
231
- /**
232
- * @dev Requests and approves a time lock period update using a meta-transaction
233
- * @param metaTx The meta-transaction
234
- * @return The transaction ID
235
- */
236
- function updateTimeLockRequestAndApprove(
237
- EngineBlox.MetaTransaction memory metaTx
238
- ) public returns (uint256) {
239
- _validateBroadcasterAndOwnerSigner(metaTx);
240
- EngineBlox.TxRecord memory txRecord = _requestAndApproveTransaction(metaTx);
241
- return txRecord.txId;
242
- }
243
-
244
- // Execution Functions
245
- /**
246
- * @dev External function that can only be called by the contract itself to execute ownership transfer
247
- * @param newOwner The new owner address
248
- */
249
- function executeTransferOwnership(address newOwner) external {
250
- _validateExecuteBySelf();
251
- _transferOwnership(newOwner);
252
- }
253
-
254
- /**
255
- * @dev External function that can only be called by the contract itself to execute broadcaster update
256
- * @param newBroadcaster The new broadcaster address (zero address to revoke at location)
257
- * @param location The index in the broadcaster role's authorized wallets set
258
- */
259
- function executeBroadcasterUpdate(address newBroadcaster, uint256 location) external {
260
- _validateExecuteBySelf();
261
- _updateBroadcaster(newBroadcaster, location);
262
- }
263
-
264
- /**
265
- * @dev External function that can only be called by the contract itself to execute recovery update
266
- * @param newRecoveryAddress The new recovery address
267
- */
268
- function executeRecoveryUpdate(address newRecoveryAddress) external {
269
- _validateExecuteBySelf();
270
- _updateRecoveryAddress(newRecoveryAddress);
271
- }
272
-
273
- /**
274
- * @dev External function that can only be called by the contract itself to execute timelock update
275
- * @param newTimeLockPeriodSec The new timelock period in seconds
276
- */
277
- function executeTimeLockUpdate(uint256 newTimeLockPeriodSec) external {
278
- _validateExecuteBySelf();
279
- _updateTimeLockPeriod(newTimeLockPeriodSec);
280
- }
281
-
282
- // ============ INTERNAL FUNCTIONS ============
283
-
284
- /**
285
- * @dev Reverts if an ownership-transfer or broadcaster-update request is already pending.
286
- */
287
- function _requireNoPendingRequest() internal view {
288
- if (_hasOpenRequest) revert SharedValidation.PendingSecureRequest();
289
- }
290
-
291
- /**
292
- * @dev Validates that the caller is the broadcaster and that the meta-tx signer is the owner.
293
- * @param metaTx The meta-transaction to validate
294
- */
295
- function _validateBroadcasterAndOwnerSigner(EngineBlox.MetaTransaction memory metaTx) internal view {
296
- _validateBroadcaster(msg.sender);
297
- SharedValidation.validateOwnerIsSigner(metaTx.params.signer, owner());
298
- }
299
-
300
- /**
301
- * @dev Completes ownership/broadcaster flow after approval: resets flag and returns txId.
302
- * @param updatedRecord The updated transaction record from approval
303
- * @return txId The transaction ID
304
- */
305
- function _completeApprove(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
306
- _hasOpenRequest = false;
307
- return updatedRecord.txId;
308
- }
309
-
310
- /**
311
- * @dev Completes ownership/broadcaster flow after cancellation: resets flag, logs txId, returns txId.
312
- * @param updatedRecord The updated transaction record from cancellation
313
- * @return txId The transaction ID
314
- */
315
- function _completeCancel(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
316
- _hasOpenRequest = false;
317
- return updatedRecord.txId;
318
- }
319
-
320
- /**
321
- * @dev Transfers ownership of the contract
322
- * @param newOwner The new owner of the contract
323
- */
324
- function _transferOwnership(address newOwner) internal virtual {
325
- address oldOwner = owner();
326
- _updateWallet(EngineBlox.OWNER_ROLE, newOwner, oldOwner);
327
- _logAddressPairEvent(oldOwner, newOwner);
328
- }
329
-
330
- /**
331
- * @dev Updates the broadcaster role at a specific index (location)
332
- * @param newBroadcaster The new broadcaster address (zero address to revoke)
333
- * @param location The index in the broadcaster role's authorized wallets set
334
- *
335
- * Logic:
336
- * - If a broadcaster exists at `location` and `newBroadcaster` is non-zero,
337
- * update that slot from old to new (role remains full).
338
- * - If no broadcaster exists at `location` and `newBroadcaster` is non-zero,
339
- * assign `newBroadcaster` to the broadcaster role (respecting maxWallets).
340
- * - If `newBroadcaster` is the zero address and a broadcaster exists at `location`,
341
- * revoke that broadcaster from the role.
342
- */
343
- function _updateBroadcaster(address newBroadcaster, uint256 location) internal virtual {
344
- EngineBlox.Role storage role = _getSecureState().roles[EngineBlox.BROADCASTER_ROLE];
345
-
346
- address oldBroadcaster;
347
- uint256 length = role.walletCount;
348
-
349
- if (location < length) {
350
- oldBroadcaster = _getAuthorizedWalletAt(EngineBlox.BROADCASTER_ROLE, location);
351
- } else {
352
- oldBroadcaster = address(0);
353
- }
354
-
355
- // Case 1: Revoke existing broadcaster at location
356
- if (newBroadcaster == address(0)) {
357
- if (oldBroadcaster != address(0)) {
358
- _revokeWallet(EngineBlox.BROADCASTER_ROLE, oldBroadcaster);
359
- _logAddressPairEvent(oldBroadcaster, address(0));
360
- }
361
- return;
362
- }
363
-
364
- // Case 2: Update existing broadcaster at location
365
- if (oldBroadcaster != address(0)) {
366
- _updateWallet(EngineBlox.BROADCASTER_ROLE, newBroadcaster, oldBroadcaster);
367
- _logAddressPairEvent(oldBroadcaster, newBroadcaster);
368
- return;
369
- }
370
-
371
- // Case 3: No broadcaster at location, assign a new one (will respect maxWallets)
372
- _assignWallet(EngineBlox.BROADCASTER_ROLE, newBroadcaster);
373
- _logAddressPairEvent(address(0), newBroadcaster);
374
- }
375
-
376
- /**
377
- * @dev Updates the recovery address
378
- * @param newRecoveryAddress The new recovery address
379
- */
380
- function _updateRecoveryAddress(address newRecoveryAddress) internal virtual {
381
- address oldRecovery = getRecovery();
382
- _updateWallet(EngineBlox.RECOVERY_ROLE, newRecoveryAddress, oldRecovery);
383
- _logAddressPairEvent(oldRecovery, newRecoveryAddress);
384
- }
385
-
386
- /**
387
- * @dev Emits ComponentEvent with ABI-encoded (address, address) payload. Reused to reduce contract size.
388
- * @param a First address
389
- * @param b Second address
390
- */
391
- function _logAddressPairEvent(address a, address b) internal {
392
- _logComponentEvent(abi.encode(a, b));
393
- }
394
- }
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ pragma solidity 0.8.34;
3
+
4
+ // Contracts imports
5
+ import "../base/BaseStateMachine.sol";
6
+ import "./lib/definitions/SecureOwnableDefinitions.sol";
7
+ import "../lib/interfaces/IDefinition.sol";
8
+ import "../lib/utils/SharedValidation.sol";
9
+ import "./interface/ISecureOwnable.sol";
10
+
11
+ /**
12
+ * @title SecureOwnable
13
+ * @dev Security-focused contract extending BaseStateMachine with ownership management
14
+ *
15
+ * SecureOwnable provides security-specific functionality built on top of the base state machine:
16
+ * - Multi-role security model with Owner, Broadcaster, and Recovery roles
17
+ * - Secure ownership transfer with time-locked operations
18
+ * - Broadcaster and recovery address management
19
+ * - Time-lock period configuration
20
+ *
21
+ * The contract implements four primary secure operation types:
22
+ * 1. OWNERSHIP_TRANSFER - For securely transferring contract ownership
23
+ * 2. BROADCASTER_UPDATE - For changing the broadcaster address
24
+ * 3. RECOVERY_UPDATE - For updating the recovery address
25
+ * 4. TIMELOCK_UPDATE - For modifying the time lock period
26
+ *
27
+ * Each operation follows a request -> approval workflow with appropriate time locks
28
+ * and authorization checks. Operations can be cancelled within specific time windows.
29
+ *
30
+ * Pending secure requests use separate flags for ownership transfer and broadcaster update.
31
+ * A new ownership-transfer request is allowed if no ownership transfer is already pending
32
+ * (a broadcaster update may still be pending). A new broadcaster-update request is allowed only
33
+ * when neither type has a pending request.
34
+ *
35
+ * This contract focuses purely on security logic while leveraging the BaseStateMachine
36
+ * for transaction management, meta-transactions, and state machine operations.
37
+ */
38
+ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
39
+ using SharedValidation for *;
40
+
41
+ /// @dev Tracks pending secure txs by type. Upgrading from legacy `_hasOpenRequest` / `_pendingBits` requires no pending requests.
42
+ bool private _hasOpenOwnershipRequest;
43
+ bool private _hasOpenBroadcasterRequest;
44
+
45
+ /**
46
+ * @notice Initializer to initialize SecureOwnable state
47
+ * @param initialOwner The initial owner address
48
+ * @param broadcaster The broadcaster address
49
+ * @param recovery The recovery address
50
+ * @param timeLockPeriodSec The timelock period in seconds
51
+ * @param eventForwarder The event forwarder address
52
+ */
53
+ function initialize(
54
+ address initialOwner,
55
+ address broadcaster,
56
+ address recovery,
57
+ uint256 timeLockPeriodSec,
58
+ address eventForwarder
59
+ ) public virtual onlyInitializing {
60
+ _initializeBaseStateMachine(initialOwner, broadcaster, recovery, timeLockPeriodSec, eventForwarder);
61
+
62
+ // Load SecureOwnable-specific definitions
63
+ IDefinition.RolePermission memory secureOwnablePermissions = SecureOwnableDefinitions.getRolePermissions();
64
+ _loadDefinitions(
65
+ SecureOwnableDefinitions.getFunctionSchemas(),
66
+ secureOwnablePermissions.roleHashes,
67
+ secureOwnablePermissions.functionPermissions,
68
+ true // Enforce all function schemas are protected
69
+ );
70
+ }
71
+
72
+ // ============ INTERFACE SUPPORT ============
73
+
74
+ /**
75
+ * @dev See {IERC165-supportsInterface}.
76
+ * @notice Adds ISecureOwnable interface ID for component detection
77
+ */
78
+ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
79
+ return interfaceId == type(ISecureOwnable).interfaceId || super.supportsInterface(interfaceId);
80
+ }
81
+
82
+ // Ownership Management
83
+ /**
84
+ * @dev Requests a transfer of ownership
85
+ * @return txId The transaction ID (use getTransaction(txId) for full record)
86
+ */
87
+ function transferOwnershipRequest() public returns (uint256 txId) {
88
+ SharedValidation.validateRecovery(getRecovery());
89
+ _requireNoPendingRequest(SecureOwnableDefinitions.OWNERSHIP_TRANSFER);
90
+
91
+ EngineBlox.TxRecord memory txRecord = _requestTransaction(
92
+ msg.sender,
93
+ address(this),
94
+ 0, // value
95
+ 0, // no gas limit
96
+ SecureOwnableDefinitions.OWNERSHIP_TRANSFER,
97
+ SecureOwnableDefinitions.TRANSFER_OWNERSHIP_SELECTOR,
98
+ abi.encode(getRecovery())
99
+ );
100
+
101
+ _hasOpenOwnershipRequest = true;
102
+ _logAddressPairEvent(owner(), getRecovery());
103
+ return txRecord.txId;
104
+ }
105
+
106
+ /**
107
+ * @dev Approves a pending ownership transfer transaction after the release time
108
+ * @param txId The transaction ID
109
+ * @return The transaction ID
110
+ */
111
+ function transferOwnershipDelayedApproval(uint256 txId) public returns (uint256) {
112
+ SharedValidation.validateOwnerOrRecovery(owner(), getRecovery());
113
+ return _completeApprove(_approveTransaction(txId));
114
+ }
115
+
116
+ /**
117
+ * @dev Approves a pending ownership transfer transaction using a meta-transaction
118
+ * @param metaTx The meta-transaction
119
+ * @return The transaction ID
120
+ */
121
+ function transferOwnershipApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
122
+ _validateBroadcasterAndOwnerSigner(metaTx);
123
+ return _completeApprove(_approveTransactionWithMetaTx(metaTx));
124
+ }
125
+
126
+ /**
127
+ * @dev Cancels a pending ownership transfer transaction
128
+ * @param txId The transaction ID
129
+ * @return The transaction ID
130
+ */
131
+ function transferOwnershipCancellation(uint256 txId) public returns (uint256) {
132
+ SharedValidation.validateRecovery(getRecovery());
133
+ return _completeCancel(_cancelTransaction(txId));
134
+ }
135
+
136
+ /**
137
+ * @dev Cancels a pending ownership transfer transaction using a meta-transaction
138
+ * @param metaTx The meta-transaction
139
+ * @return The transaction ID
140
+ */
141
+ function transferOwnershipCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
142
+ _validateBroadcasterAndOwnerSigner(metaTx);
143
+ return _completeCancel(_cancelTransactionWithMetaTx(metaTx));
144
+ }
145
+
146
+ // Broadcaster Management
147
+ /**
148
+ * @dev Requests an update to the broadcaster at a specific location (index).
149
+ * @notice Requires no pending broadcaster-update and no pending ownership-transfer request.
150
+ * @param newBroadcaster The new broadcaster address (zero address to revoke at location)
151
+ * @param location The index in the broadcaster role's authorized wallets set
152
+ * @return txId The transaction ID for the pending request (use getTransaction(txId) for full record)
153
+ */
154
+ function updateBroadcasterRequest(address newBroadcaster, uint256 location) public returns (uint256 txId) {
155
+ SharedValidation.validateOwner(owner());
156
+ _requireNoPendingRequest(SecureOwnableDefinitions.BROADCASTER_UPDATE);
157
+ _requireNoPendingRequest(SecureOwnableDefinitions.OWNERSHIP_TRANSFER);
158
+
159
+ // Get the current broadcaster at the specified location. zero address if no broadcaster at location.
160
+ address currentBroadcaster = location < _getSecureState().roles[EngineBlox.BROADCASTER_ROLE].walletCount
161
+ ? _getAuthorizedWalletAt(EngineBlox.BROADCASTER_ROLE, location)
162
+ : address(0);
163
+
164
+ EngineBlox.TxRecord memory txRecord = _requestTransaction(
165
+ msg.sender,
166
+ address(this),
167
+ 0, // value
168
+ 0, // gas limit
169
+ SecureOwnableDefinitions.BROADCASTER_UPDATE,
170
+ SecureOwnableDefinitions.UPDATE_BROADCASTER_SELECTOR,
171
+ abi.encode(newBroadcaster, location)
172
+ );
173
+
174
+ _hasOpenBroadcasterRequest = true;
175
+ _logAddressPairEvent(currentBroadcaster, newBroadcaster);
176
+ return txRecord.txId;
177
+ }
178
+
179
+ /**
180
+ * @dev Approves a pending broadcaster update transaction after the release time
181
+ * @param txId The transaction ID
182
+ * @return The transaction ID
183
+ */
184
+ function updateBroadcasterDelayedApproval(uint256 txId) public returns (uint256) {
185
+ SharedValidation.validateOwner(owner());
186
+ return _completeApprove(_approveTransaction(txId));
187
+ }
188
+
189
+ /**
190
+ * @dev Approves a pending broadcaster update transaction using a meta-transaction
191
+ * @param metaTx The meta-transaction
192
+ * @return The transaction ID
193
+ */
194
+ function updateBroadcasterApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
195
+ _validateBroadcasterAndOwnerSigner(metaTx);
196
+ return _completeApprove(_approveTransactionWithMetaTx(metaTx));
197
+ }
198
+
199
+ /**
200
+ * @dev Cancels a pending broadcaster update transaction
201
+ * @param txId The transaction ID
202
+ * @return The transaction ID
203
+ */
204
+ function updateBroadcasterCancellation(uint256 txId) public returns (uint256) {
205
+ SharedValidation.validateOwner(owner());
206
+ return _completeCancel(_cancelTransaction(txId));
207
+ }
208
+
209
+ /**
210
+ * @dev Cancels a pending broadcaster update transaction using a meta-transaction
211
+ * @param metaTx The meta-transaction
212
+ * @return The transaction ID
213
+ */
214
+ function updateBroadcasterCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
215
+ _validateBroadcasterAndOwnerSigner(metaTx);
216
+ return _completeCancel(_cancelTransactionWithMetaTx(metaTx));
217
+ }
218
+
219
+ // Recovery Management
220
+
221
+ /**
222
+ * @dev Requests and approves a recovery address update using a meta-transaction
223
+ * @param metaTx The meta-transaction
224
+ * @return The transaction ID
225
+ */
226
+ function updateRecoveryRequestAndApprove(
227
+ EngineBlox.MetaTransaction memory metaTx
228
+ ) public returns (uint256) {
229
+ _validateBroadcasterAndOwnerSigner(metaTx);
230
+ EngineBlox.TxRecord memory txRecord = _requestAndApproveTransaction(metaTx);
231
+ return txRecord.txId;
232
+ }
233
+
234
+ // TimeLock Management
235
+
236
+ /**
237
+ * @dev Requests and approves a time lock period update using a meta-transaction
238
+ * @param metaTx The meta-transaction
239
+ * @return The transaction ID
240
+ */
241
+ function updateTimeLockRequestAndApprove(
242
+ EngineBlox.MetaTransaction memory metaTx
243
+ ) public returns (uint256) {
244
+ _validateBroadcasterAndOwnerSigner(metaTx);
245
+ EngineBlox.TxRecord memory txRecord = _requestAndApproveTransaction(metaTx);
246
+ return txRecord.txId;
247
+ }
248
+
249
+ // Execution Functions
250
+ /**
251
+ * @dev External function that can only be called by the contract itself to execute ownership transfer
252
+ * @param newOwner The new owner address
253
+ */
254
+ function executeTransferOwnership(address newOwner) external {
255
+ _validateExecuteBySelf();
256
+ _transferOwnership(newOwner);
257
+ }
258
+
259
+ /**
260
+ * @dev External function that can only be called by the contract itself to execute broadcaster update
261
+ * @param newBroadcaster The new broadcaster address (zero address to revoke at location)
262
+ * @param location The index in the broadcaster role's authorized wallets set
263
+ */
264
+ function executeBroadcasterUpdate(address newBroadcaster, uint256 location) external {
265
+ _validateExecuteBySelf();
266
+ _updateBroadcaster(newBroadcaster, location);
267
+ }
268
+
269
+ /**
270
+ * @dev External function that can only be called by the contract itself to execute recovery update
271
+ * @param newRecoveryAddress The new recovery address
272
+ */
273
+ function executeRecoveryUpdate(address newRecoveryAddress) external {
274
+ _validateExecuteBySelf();
275
+ _updateRecoveryAddress(newRecoveryAddress);
276
+ }
277
+
278
+ /**
279
+ * @dev External function that can only be called by the contract itself to execute timelock update
280
+ * @param newTimeLockPeriodSec The new timelock period in seconds
281
+ */
282
+ function executeTimeLockUpdate(uint256 newTimeLockPeriodSec) external {
283
+ _validateExecuteBySelf();
284
+ _updateTimeLockPeriod(newTimeLockPeriodSec);
285
+ }
286
+
287
+ // ============ INTERNAL FUNCTIONS ============
288
+
289
+
290
+ /**
291
+ * @dev Validates that the caller is the broadcaster and that the meta-tx signer is the owner.
292
+ * @param metaTx The meta-transaction to validate
293
+ */
294
+ function _validateBroadcasterAndOwnerSigner(EngineBlox.MetaTransaction memory metaTx) internal view {
295
+ _validateBroadcaster(msg.sender);
296
+ SharedValidation.validateOwnerIsSigner(metaTx.params.signer, owner());
297
+ }
298
+
299
+ /**
300
+ * @dev Completes ownership/broadcaster flow after approval: clears the matching pending flag and returns txId.
301
+ * @param updatedRecord The updated transaction record from approval
302
+ * @return txId The transaction ID
303
+ */
304
+ function _completeApprove(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
305
+ _clearPendingFlagForOperation(updatedRecord.params.operationType);
306
+ return updatedRecord.txId;
307
+ }
308
+
309
+ /**
310
+ * @dev Completes ownership/broadcaster flow after cancellation: clears the matching pending flag and returns txId.
311
+ * @param updatedRecord The updated transaction record from cancellation
312
+ * @return txId The transaction ID
313
+ */
314
+ function _completeCancel(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
315
+ _clearPendingFlagForOperation(updatedRecord.params.operationType);
316
+ return updatedRecord.txId;
317
+ }
318
+
319
+ /**
320
+ * @dev Reverts if the pending flag for `requestOperationType` is already set (one lane per call).
321
+ * `OWNERSHIP_TRANSFER` checks only `_hasOpenOwnershipRequest` (a broadcaster update may still be pending).
322
+ * `BROADCASTER_UPDATE` checks only `_hasOpenBroadcasterRequest`. Callers that need both lanes idle
323
+ * (e.g. `updateBroadcasterRequest`) invoke this once per operation type.
324
+ * @param requestOperationType Lane to validate (`OWNERSHIP_TRANSFER` or `BROADCASTER_UPDATE`).
325
+ */
326
+ function _requireNoPendingRequest(bytes32 requestOperationType) internal view {
327
+ if (requestOperationType == SecureOwnableDefinitions.OWNERSHIP_TRANSFER) {
328
+ if (_hasOpenOwnershipRequest) revert SharedValidation.PendingSecureRequest();
329
+ } else if (requestOperationType == SecureOwnableDefinitions.BROADCASTER_UPDATE) {
330
+ if (_hasOpenBroadcasterRequest) revert SharedValidation.PendingSecureRequest();
331
+ } else {
332
+ revert();
333
+ }
334
+ }
335
+
336
+ /**
337
+ * @dev Clears the pending flag for a completed or cancelled secure op (approve/cancel paths).
338
+ * @param operationType The tx record's `operationType` (`OWNERSHIP_TRANSFER` or `BROADCASTER_UPDATE`).
339
+ */
340
+ function _clearPendingFlagForOperation(bytes32 operationType) private {
341
+ if (operationType == SecureOwnableDefinitions.OWNERSHIP_TRANSFER) {
342
+ _hasOpenOwnershipRequest = false;
343
+ } else if (operationType == SecureOwnableDefinitions.BROADCASTER_UPDATE) {
344
+ _hasOpenBroadcasterRequest = false;
345
+ } else {
346
+ revert();
347
+ }
348
+ }
349
+
350
+ /**
351
+ * @dev Transfers ownership of the contract
352
+ * @param newOwner The new owner of the contract
353
+ */
354
+ function _transferOwnership(address newOwner) internal virtual {
355
+ address oldOwner = owner();
356
+ _updateWallet(EngineBlox.OWNER_ROLE, newOwner, oldOwner);
357
+ _logAddressPairEvent(oldOwner, newOwner);
358
+ }
359
+
360
+ /**
361
+ * @dev Updates the broadcaster role at a specific index (location)
362
+ * @param newBroadcaster The new broadcaster address (zero address to revoke)
363
+ * @param location The index in the broadcaster role's authorized wallets set
364
+ *
365
+ * Logic:
366
+ * - If a broadcaster exists at `location` and `newBroadcaster` is non-zero,
367
+ * update that slot from old to new (role remains full).
368
+ * - If no broadcaster exists at `location` and `newBroadcaster` is non-zero,
369
+ * assign `newBroadcaster` to the broadcaster role (respecting maxWallets).
370
+ * - If `newBroadcaster` is the zero address and a broadcaster exists at `location`,
371
+ * revoke that broadcaster from the role.
372
+ */
373
+ function _updateBroadcaster(address newBroadcaster, uint256 location) internal virtual {
374
+ EngineBlox.Role storage role = _getSecureState().roles[EngineBlox.BROADCASTER_ROLE];
375
+
376
+ address oldBroadcaster;
377
+ uint256 length = role.walletCount;
378
+
379
+ if (location < length) {
380
+ oldBroadcaster = _getAuthorizedWalletAt(EngineBlox.BROADCASTER_ROLE, location);
381
+ } else {
382
+ oldBroadcaster = address(0);
383
+ }
384
+
385
+ // Case 1: Revoke existing broadcaster at location
386
+ if (newBroadcaster == address(0)) {
387
+ if (oldBroadcaster != address(0)) {
388
+ _revokeWallet(EngineBlox.BROADCASTER_ROLE, oldBroadcaster);
389
+ _logAddressPairEvent(oldBroadcaster, address(0));
390
+ }
391
+ return;
392
+ }
393
+
394
+ // Case 2: Update existing broadcaster at location
395
+ if (oldBroadcaster != address(0)) {
396
+ _updateWallet(EngineBlox.BROADCASTER_ROLE, newBroadcaster, oldBroadcaster);
397
+ _logAddressPairEvent(oldBroadcaster, newBroadcaster);
398
+ return;
399
+ }
400
+
401
+ // Case 3: No broadcaster at location, assign a new one (will respect maxWallets)
402
+ _assignWallet(EngineBlox.BROADCASTER_ROLE, newBroadcaster);
403
+ _logAddressPairEvent(address(0), newBroadcaster);
404
+ }
405
+
406
+ /**
407
+ * @dev Updates the recovery address
408
+ * @param newRecoveryAddress The new recovery address
409
+ */
410
+ function _updateRecoveryAddress(address newRecoveryAddress) internal virtual {
411
+ address oldRecovery = getRecovery();
412
+ _updateWallet(EngineBlox.RECOVERY_ROLE, newRecoveryAddress, oldRecovery);
413
+ _logAddressPairEvent(oldRecovery, newRecoveryAddress);
414
+ }
415
+
416
+ /**
417
+ * @dev Emits ComponentEvent with ABI-encoded (address, address) payload. Reused to reduce contract size.
418
+ * @param a First address
419
+ * @param b Second address
420
+ */
421
+ function _logAddressPairEvent(address a, address b) internal {
422
+ _logComponentEvent(abi.encode(a, b));
423
+ }
424
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloxchain/contracts",
3
- "version": "1.0.0-alpha.18",
3
+ "version": "1.0.0-alpha.19",
4
4
  "description": "Library engine for building enterprise grade decentralized permissioned applications",
5
5
  "files": [
6
6
  "core",