@bloxchain/contracts 1.0.0-alpha.2 → 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.
Files changed (42) hide show
  1. package/README.md +7 -7
  2. package/abi/BaseStateMachine.abi.json +85 -45
  3. package/abi/EngineBlox.abi.json +73 -90
  4. package/abi/GuardController.abi.json +252 -806
  5. package/abi/{SimpleVaultDefinitions.abi.json → GuardControllerDefinitions.abi.json} +170 -28
  6. package/abi/IDefinition.abi.json +5 -0
  7. package/abi/RuntimeRBAC.abi.json +155 -218
  8. package/abi/RuntimeRBACDefinitions.abi.json +179 -0
  9. package/abi/SecureOwnable.abi.json +524 -1621
  10. package/abi/SecureOwnableDefinitions.abi.json +5 -0
  11. package/components/README.md +8 -0
  12. package/core/access/RuntimeRBAC.sol +255 -270
  13. package/core/access/interface/IRuntimeRBAC.sol +55 -84
  14. package/core/access/lib/definitions/RuntimeRBACDefinitions.sol +93 -2
  15. package/core/base/BaseStateMachine.sol +193 -107
  16. package/core/base/interface/IBaseStateMachine.sol +153 -153
  17. package/core/execution/GuardController.sol +155 -131
  18. package/core/execution/interface/IGuardController.sol +146 -120
  19. package/core/execution/lib/definitions/GuardControllerDefinitions.sol +193 -43
  20. package/core/lib/EngineBlox.sol +2683 -2322
  21. package/{interfaces → core/lib/interfaces}/IDefinition.sol +49 -49
  22. package/{interfaces → core/lib/interfaces}/IEventForwarder.sol +33 -33
  23. package/{utils → core/lib/utils}/SharedValidation.sol +61 -8
  24. package/core/pattern/Account.sol +84 -0
  25. package/core/security/SecureOwnable.sol +456 -412
  26. package/core/security/interface/ISecureOwnable.sol +105 -104
  27. package/core/security/lib/definitions/SecureOwnableDefinitions.sol +22 -6
  28. package/package.json +5 -5
  29. package/standards/README.md +12 -0
  30. package/standards/behavior/ICopyable.sol +34 -0
  31. package/standards/hooks/IOnActionHook.sol +21 -0
  32. package/abi/AccountBlox.abi.json +0 -5799
  33. package/abi/BareBlox.abi.json +0 -1284
  34. package/abi/RoleBlox.abi.json +0 -4209
  35. package/abi/SecureBlox.abi.json +0 -3828
  36. package/abi/SimpleRWA20.abi.json +0 -5288
  37. package/abi/SimpleRWA20Definitions.abi.json +0 -191
  38. package/abi/SimpleVault.abi.json +0 -4951
  39. package/core/research/BloxchainWallet.sol +0 -306
  40. package/core/research/erc20-blox/ERC20Blox.sol +0 -140
  41. package/core/research/erc20-blox/lib/definitions/ERC20BloxDefinitions.sol +0 -185
  42. package/interfaces/IOnActionHook.sol +0 -79
@@ -1,412 +1,456 @@
1
- // SPDX-License-Identifier: MPL-2.0
2
- pragma solidity 0.8.33;
3
-
4
- // Contracts imports
5
- import "../base/BaseStateMachine.sol";
6
- import "./lib/definitions/SecureOwnableDefinitions.sol";
7
- import "../../interfaces/IDefinition.sol";
8
- import "../../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
- // Initialize base state machine (only if not already initialized)
58
- if (!_secureState.initialized) {
59
- _initializeBaseStateMachine(initialOwner, broadcaster, recovery, timeLockPeriodSec, eventForwarder);
60
- }
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 // Allow protected schemas for factory settings
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 The transaction record
86
- */
87
- function transferOwnershipRequest() public returns (EngineBlox.TxRecord memory) {
88
- SharedValidation.validateRecovery(getRecovery());
89
- _requireNoPendingRequest();
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
- _hasOpenRequest = true;
102
- _logComponentEvent(abi.encode(owner(), getRecovery()));
103
- return txRecord;
104
- }
105
-
106
- /**
107
- * @dev Approves a pending ownership transfer transaction after the release time
108
- * @param txId The transaction ID
109
- * @return The updated transaction record
110
- */
111
- function transferOwnershipDelayedApproval(uint256 txId) public returns (EngineBlox.TxRecord memory) {
112
- SharedValidation.validateOwnerOrRecovery(owner(), getRecovery());
113
-
114
- return _completeOwnershipApprove(_approveTransaction(txId));
115
- }
116
-
117
- /**
118
- * @dev Approves a pending ownership transfer transaction using a meta-transaction
119
- * @param metaTx The meta-transaction
120
- * @return The updated transaction record
121
- */
122
- function transferOwnershipApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (EngineBlox.TxRecord memory) {
123
- _validateBroadcasterAndOwnerSigner(metaTx);
124
-
125
- return _completeOwnershipApprove(_approveTransactionWithMetaTx(metaTx));
126
- }
127
-
128
- /**
129
- * @dev Cancels a pending ownership transfer transaction
130
- * @param txId The transaction ID
131
- * @return The updated transaction record
132
- */
133
- function transferOwnershipCancellation(uint256 txId) public returns (EngineBlox.TxRecord memory) {
134
- SharedValidation.validateRecovery(getRecovery());
135
- return _completeOwnershipCancel(_cancelTransaction(txId));
136
- }
137
-
138
- /**
139
- * @dev Cancels a pending ownership transfer transaction using a meta-transaction
140
- * @param metaTx The meta-transaction
141
- * @return The updated transaction record
142
- */
143
- function transferOwnershipCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (EngineBlox.TxRecord memory) {
144
- _validateBroadcasterAndOwnerSigner(metaTx);
145
-
146
- return _completeOwnershipCancel(_cancelTransactionWithMetaTx(metaTx));
147
- }
148
-
149
- // Broadcaster Management
150
- /**
151
- * @dev Updates the broadcaster address
152
- * @param newBroadcaster The new broadcaster address
153
- * @return The execution options
154
- */
155
- function updateBroadcasterRequest(address newBroadcaster) public returns (EngineBlox.TxRecord memory) {
156
- SharedValidation.validateOwner(owner());
157
- _requireNoPendingRequest();
158
- address currentBroadcaster = _getAuthorizedWalletAt(EngineBlox.BROADCASTER_ROLE, 0);
159
- SharedValidation.validateAddressUpdate(newBroadcaster, currentBroadcaster);
160
-
161
- EngineBlox.TxRecord memory txRecord = _requestTransaction(
162
- msg.sender,
163
- address(this),
164
- 0, // value
165
- 0, // gas limit
166
- SecureOwnableDefinitions.BROADCASTER_UPDATE,
167
- SecureOwnableDefinitions.UPDATE_BROADCASTER_SELECTOR,
168
- abi.encode(newBroadcaster)
169
- );
170
-
171
- _hasOpenRequest = true;
172
- _logComponentEvent(abi.encode(currentBroadcaster, newBroadcaster));
173
- return txRecord;
174
- }
175
-
176
- /**
177
- * @dev Approves a pending broadcaster update transaction after the release time
178
- * @param txId The transaction ID
179
- * @return The updated transaction record
180
- */
181
- function updateBroadcasterDelayedApproval(uint256 txId) public returns (EngineBlox.TxRecord memory) {
182
- SharedValidation.validateOwner(owner());
183
- return _completeBroadcasterApprove(_approveTransaction(txId));
184
- }
185
-
186
- /**
187
- * @dev Approves a pending broadcaster update transaction using a meta-transaction
188
- * @param metaTx The meta-transaction
189
- * @return The updated transaction record
190
- */
191
- function updateBroadcasterApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (EngineBlox.TxRecord memory) {
192
- _validateBroadcasterAndOwnerSigner(metaTx);
193
-
194
- return _completeBroadcasterApprove(_approveTransactionWithMetaTx(metaTx));
195
- }
196
-
197
- /**
198
- * @dev Cancels a pending broadcaster update transaction
199
- * @param txId The transaction ID
200
- * @return The updated transaction record
201
- */
202
- function updateBroadcasterCancellation(uint256 txId) public returns (EngineBlox.TxRecord memory) {
203
- SharedValidation.validateOwner(owner());
204
- return _completeBroadcasterCancel(_cancelTransaction(txId));
205
- }
206
-
207
- /**
208
- * @dev Cancels a pending broadcaster update transaction using a meta-transaction
209
- * @param metaTx The meta-transaction
210
- * @return The updated transaction record
211
- */
212
- function updateBroadcasterCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (EngineBlox.TxRecord memory) {
213
- _validateBroadcasterAndOwnerSigner(metaTx);
214
-
215
- return _completeBroadcasterCancel(_cancelTransactionWithMetaTx(metaTx));
216
- }
217
-
218
- // Recovery Management
219
-
220
- /**
221
- * @dev Requests and approves a recovery address update using a meta-transaction
222
- * @param metaTx The meta-transaction
223
- * @return The transaction record
224
- */
225
- function updateRecoveryRequestAndApprove(
226
- EngineBlox.MetaTransaction memory metaTx
227
- ) public returns (EngineBlox.TxRecord memory) {
228
- _validateBroadcasterAndOwnerSigner(metaTx);
229
-
230
- return _requestAndApproveTransaction(metaTx);
231
- }
232
-
233
- // TimeLock Management
234
-
235
- /**
236
- * @dev Requests and approves a time lock period update using a meta-transaction
237
- * @param metaTx The meta-transaction
238
- * @return The transaction record
239
- */
240
- function updateTimeLockRequestAndApprove(
241
- EngineBlox.MetaTransaction memory metaTx
242
- ) public returns (EngineBlox.TxRecord memory) {
243
- _validateBroadcasterAndOwnerSigner(metaTx);
244
-
245
- return _requestAndApproveTransaction(metaTx);
246
- }
247
-
248
- // Execution Functions
249
- /**
250
- * @dev External function that can only be called by the contract itself to execute ownership transfer
251
- * @param newOwner The new owner address
252
- */
253
- function executeTransferOwnership(address newOwner) external {
254
- _validateExecuteBySelf();
255
- _transferOwnership(newOwner);
256
- }
257
-
258
- /**
259
- * @dev External function that can only be called by the contract itself to execute broadcaster update
260
- * @param newBroadcaster The new broadcaster address
261
- */
262
- function executeBroadcasterUpdate(address newBroadcaster) external {
263
- _validateExecuteBySelf();
264
- _updateBroadcaster(newBroadcaster, 0);
265
- }
266
-
267
- /**
268
- * @dev External function that can only be called by the contract itself to execute recovery update
269
- * @param newRecoveryAddress The new recovery address
270
- */
271
- function executeRecoveryUpdate(address newRecoveryAddress) external {
272
- _validateExecuteBySelf();
273
- _updateRecoveryAddress(newRecoveryAddress);
274
- }
275
-
276
- /**
277
- * @dev External function that can only be called by the contract itself to execute timelock update
278
- * @param newTimeLockPeriodSec The new timelock period in seconds
279
- */
280
- function executeTimeLockUpdate(uint256 newTimeLockPeriodSec) external {
281
- _validateExecuteBySelf();
282
- _updateTimeLockPeriod(newTimeLockPeriodSec);
283
- }
284
-
285
- // ============ INTERNAL FUNCTIONS ============
286
-
287
- /**
288
- * @dev Reverts if an ownership-transfer or broadcaster-update request is already pending.
289
- */
290
- function _requireNoPendingRequest() internal view {
291
- if (_hasOpenRequest) revert SharedValidation.PendingSecureRequest();
292
- }
293
-
294
- /**
295
- * @dev Validates that the caller is the broadcaster and that the meta-tx signer is the owner.
296
- * @param metaTx The meta-transaction to validate
297
- */
298
- function _validateBroadcasterAndOwnerSigner(EngineBlox.MetaTransaction memory metaTx) internal view {
299
- _validateBroadcaster(msg.sender);
300
- SharedValidation.validateOwnerIsSigner(metaTx.params.signer, owner());
301
- }
302
-
303
- /**
304
- * @dev Completes ownership flow after approval: resets flag and returns record.
305
- */
306
- function _completeOwnershipApprove(EngineBlox.TxRecord memory updatedRecord) internal returns (EngineBlox.TxRecord memory) {
307
- _hasOpenRequest = false;
308
- return updatedRecord;
309
- }
310
-
311
- /**
312
- * @dev Completes ownership flow after cancellation: resets flag, logs txId, returns record.
313
- */
314
- function _completeOwnershipCancel(EngineBlox.TxRecord memory updatedRecord) internal returns (EngineBlox.TxRecord memory) {
315
- _hasOpenRequest = false;
316
- _logComponentEvent(abi.encode(updatedRecord.txId));
317
- return updatedRecord;
318
- }
319
-
320
- /**
321
- * @dev Completes broadcaster flow after approval: resets flag and returns record.
322
- */
323
- function _completeBroadcasterApprove(EngineBlox.TxRecord memory updatedRecord) internal returns (EngineBlox.TxRecord memory) {
324
- _hasOpenRequest = false;
325
- return updatedRecord;
326
- }
327
-
328
- /**
329
- * @dev Completes broadcaster flow after cancellation: resets flag, logs txId, returns record.
330
- */
331
- function _completeBroadcasterCancel(EngineBlox.TxRecord memory updatedRecord) internal returns (EngineBlox.TxRecord memory) {
332
- _hasOpenRequest = false;
333
- _logComponentEvent(abi.encode(updatedRecord.txId));
334
- return updatedRecord;
335
- }
336
-
337
- /**
338
- * @dev Transfers ownership of the contract
339
- * @param newOwner The new owner of the contract
340
- */
341
- function _transferOwnership(address newOwner) internal virtual {
342
- address oldOwner = owner();
343
- _updateAssignedWallet(EngineBlox.OWNER_ROLE, newOwner, oldOwner);
344
- _logComponentEvent(abi.encode(oldOwner, newOwner));
345
- }
346
-
347
- /**
348
- * @dev Updates the broadcaster role at a specific index (location)
349
- * @param newBroadcaster The new broadcaster address (zero address to revoke)
350
- * @param location The index in the broadcaster role's authorized wallets set
351
- *
352
- * Logic:
353
- * - If a broadcaster exists at `location` and `newBroadcaster` is non-zero,
354
- * update that slot from old to new (role remains full).
355
- * - If no broadcaster exists at `location` and `newBroadcaster` is non-zero,
356
- * assign `newBroadcaster` to the broadcaster role (respecting maxWallets).
357
- * - If `newBroadcaster` is the zero address and a broadcaster exists at `location`,
358
- * revoke that broadcaster from the role.
359
- */
360
- function _updateBroadcaster(address newBroadcaster, uint256 location) internal virtual {
361
- EngineBlox.Role storage role = _getSecureState().roles[EngineBlox.BROADCASTER_ROLE];
362
-
363
- address oldBroadcaster;
364
- uint256 length = role.walletCount;
365
-
366
- if (location < length) {
367
- oldBroadcaster = _getAuthorizedWalletAt(EngineBlox.BROADCASTER_ROLE, location);
368
- } else {
369
- oldBroadcaster = address(0);
370
- }
371
-
372
- // Case 1: Revoke existing broadcaster at location
373
- if (newBroadcaster == address(0)) {
374
- if (oldBroadcaster != address(0)) {
375
- _revokeWallet(EngineBlox.BROADCASTER_ROLE, oldBroadcaster);
376
- _logComponentEvent(abi.encode(oldBroadcaster, address(0)));
377
- }
378
- return;
379
- }
380
-
381
- // Case 2: Update existing broadcaster at location
382
- if (oldBroadcaster != address(0)) {
383
- _updateAssignedWallet(EngineBlox.BROADCASTER_ROLE, newBroadcaster, oldBroadcaster);
384
- _logComponentEvent(abi.encode(oldBroadcaster, newBroadcaster));
385
- return;
386
- }
387
-
388
- // Case 3: No broadcaster at location, assign a new one (will respect maxWallets)
389
- _assignWallet(EngineBlox.BROADCASTER_ROLE, newBroadcaster);
390
- _logComponentEvent(abi.encode(address(0), newBroadcaster));
391
- }
392
-
393
- /**
394
- * @dev Updates the recovery address
395
- * @param newRecoveryAddress The new recovery address
396
- */
397
- function _updateRecoveryAddress(address newRecoveryAddress) internal virtual {
398
- address oldRecovery = getRecovery();
399
- _updateAssignedWallet(EngineBlox.RECOVERY_ROLE, newRecoveryAddress, oldRecovery);
400
- _logComponentEvent(abi.encode(oldRecovery, newRecoveryAddress));
401
- }
402
-
403
- /**
404
- * @dev Updates the time lock period
405
- * @param newTimeLockPeriodSec The new time lock period in seconds
406
- */
407
- function _updateTimeLockPeriod(uint256 newTimeLockPeriodSec) internal virtual override {
408
- uint256 oldPeriod = getTimeLockPeriodSec();
409
- super._updateTimeLockPeriod(newTimeLockPeriodSec);
410
- _logComponentEvent(abi.encode(oldPeriod, newTimeLockPeriodSec));
411
- }
412
- }
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
+ * **Ownership transfer vs recovery (threat model):**
36
+ * - `transferOwnershipRequest` snapshots `getRecovery()` into the pending tx `executionParams`. On execution,
37
+ * `executeTransferOwnership` receives that snapshotted address as the new owner. Rotating recovery after
38
+ * the request does **not** rewrite the pending payload; the beneficiary remains the recovery address
39
+ * at request time.
40
+ * - `transferOwnershipDelayedApproval` authorizes the **current** owner or **current** recovery (`getRecovery()`
41
+ * at approval time). It does **not** require the approver to match the snapshotted beneficiary. Integrators
42
+ * must treat approval as consent to execute the **stored** transfer, not “transfer to whoever is recovery now.”
43
+ * - `transferOwnershipCancellation` allows only the **current** recovery to cancel. If owner and broadcaster
44
+ * rotate recovery via `updateRecoveryRequestAndApprove` while a transfer is pending, the **previous**
45
+ * recovery loses cancel rights immediately; the pending tx still targets the old address until approved,
46
+ * cancelled by the new recovery, or superseded operationally.
47
+ * - Recovery and timelock updates use a request-and-approve meta-tx path without an additional timelock and
48
+ * are **not** blocked when an ownership transfer is pending (unlike broadcaster update requests). This is
49
+ * intentional: fast recovery rotation when owner and broadcaster still cooperate; operators who need a
50
+ * strict “recovery cannot change during pending ownership transfer” invariant must enforce it off-chain or
51
+ * extend this contract.
52
+ *
53
+ * This contract focuses purely on security logic while leveraging the BaseStateMachine
54
+ * for transaction management, meta-transactions, and state machine operations.
55
+ */
56
+ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
57
+ using SharedValidation for *;
58
+
59
+ /// @dev Lane flags for **delayed** ownership-transfer and broadcaster-update requests only (`transferOwnershipRequest`,
60
+ /// `updateBroadcasterRequest`). Recovery and timelock updates use `_requestAndApproveTransaction` and do **not**
61
+ /// read or write these booleans. Each flag is set only after a successful `_requestTransaction` in that same tx;
62
+ /// clearing happens only in `_completeApprove` / `_completeCancel` in the **same** transaction as a successful
63
+ /// `_approveTransaction` / `_cancelTransaction`, so a revert unwinds engine state and flag writes together.
64
+ /// @dev Upgrading from legacy `_hasOpenRequest` / `_pendingBits` requires no pending requests.
65
+ bool private _hasOpenOwnershipRequest;
66
+ bool private _hasOpenBroadcasterRequest;
67
+
68
+ /**
69
+ * @notice Initializer to initialize SecureOwnable state
70
+ * @param initialOwner The initial owner address
71
+ * @param broadcaster The broadcaster address
72
+ * @param recovery The recovery address
73
+ * @param timeLockPeriodSec The timelock period in seconds
74
+ * @param eventForwarder The event forwarder address
75
+ */
76
+ function initialize(
77
+ address initialOwner,
78
+ address broadcaster,
79
+ address recovery,
80
+ uint256 timeLockPeriodSec,
81
+ address eventForwarder
82
+ ) public virtual onlyInitializing {
83
+ _initializeBaseStateMachine(initialOwner, broadcaster, recovery, timeLockPeriodSec, eventForwarder);
84
+
85
+ // Load SecureOwnable-specific definitions
86
+ IDefinition.RolePermission memory secureOwnablePermissions = SecureOwnableDefinitions.getRolePermissions();
87
+ _loadDefinitions(
88
+ SecureOwnableDefinitions.getFunctionSchemas(),
89
+ secureOwnablePermissions.roleHashes,
90
+ secureOwnablePermissions.functionPermissions,
91
+ true // Enforce all function schemas are protected
92
+ );
93
+ }
94
+
95
+ // ============ INTERFACE SUPPORT ============
96
+
97
+ /**
98
+ * @dev See {IERC165-supportsInterface}.
99
+ * @notice Adds ISecureOwnable interface ID for component detection
100
+ */
101
+ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
102
+ return interfaceId == type(ISecureOwnable).interfaceId || super.supportsInterface(interfaceId);
103
+ }
104
+
105
+ // Ownership Management
106
+ /**
107
+ * @dev Requests a time-delayed transfer of the OWNER role to the **recovery address at request time**.
108
+ * @notice Encodes `getRecovery()` into `executionParams`; that address becomes the new owner on successful
109
+ * execution. Changing recovery later does not update this pending record.
110
+ * @return txId The transaction ID (use getTransaction(txId) for full record)
111
+ */
112
+ function transferOwnershipRequest() public returns (uint256 txId) {
113
+ SharedValidation.validateRecovery(getRecovery());
114
+ _requireNoPendingRequest(SecureOwnableDefinitions.OWNERSHIP_TRANSFER);
115
+
116
+ EngineBlox.TxRecord memory txRecord = _requestTransaction(
117
+ msg.sender,
118
+ address(this),
119
+ 0, // value
120
+ 0, // no gas limit
121
+ SecureOwnableDefinitions.OWNERSHIP_TRANSFER,
122
+ SecureOwnableDefinitions.TRANSFER_OWNERSHIP_SELECTOR,
123
+ abi.encode(getRecovery())
124
+ );
125
+
126
+ _hasOpenOwnershipRequest = true;
127
+ _logAddressPairEvent(owner(), getRecovery());
128
+ return txRecord.txId;
129
+ }
130
+
131
+ /**
132
+ * @dev Approves a pending ownership transfer after `releaseTime` (timelock on the direct path).
133
+ * @notice Callable by **current** owner or **current** recovery. Execution still transfers ownership to
134
+ * the address snapshotted at request time, which may differ from `getRecovery()` at approval time.
135
+ * @param txId The transaction ID
136
+ * @return The transaction ID
137
+ */
138
+ function transferOwnershipDelayedApproval(uint256 txId) public returns (uint256) {
139
+ SharedValidation.validateOwnerOrRecovery(owner(), getRecovery());
140
+ return _completeApprove(_approveTransaction(txId));
141
+ }
142
+
143
+ /**
144
+ * @dev Approves a pending ownership transfer transaction using a meta-transaction
145
+ * @param metaTx The meta-transaction
146
+ * @return The transaction ID
147
+ */
148
+ function transferOwnershipApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
149
+ _validateBroadcasterAndOwnerSigner(metaTx);
150
+ return _completeApprove(_approveTransactionWithMetaTx(metaTx));
151
+ }
152
+
153
+ /**
154
+ * @dev Cancels a pending ownership transfer transaction.
155
+ * @notice Only the **current** `getRecovery()` may cancel. After a recovery rotation, the prior recovery
156
+ * address can no longer cancel.
157
+ * @param txId The transaction ID
158
+ * @return The transaction ID
159
+ */
160
+ function transferOwnershipCancellation(uint256 txId) public returns (uint256) {
161
+ SharedValidation.validateRecovery(getRecovery());
162
+ return _completeCancel(_cancelTransaction(txId));
163
+ }
164
+
165
+ /**
166
+ * @dev Cancels a pending ownership transfer transaction using a meta-transaction
167
+ * @param metaTx The meta-transaction
168
+ * @return The transaction ID
169
+ */
170
+ function transferOwnershipCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
171
+ _validateBroadcasterAndOwnerSigner(metaTx);
172
+ return _completeCancel(_cancelTransactionWithMetaTx(metaTx));
173
+ }
174
+
175
+ // Broadcaster Management
176
+ /**
177
+ * @dev Requests an update to the broadcaster at a specific location (index).
178
+ * @notice Requires no pending broadcaster-update and no pending ownership-transfer request.
179
+ * @param newBroadcaster The new broadcaster address (zero address to revoke at location)
180
+ * @param location The index in the broadcaster role's authorized wallets set
181
+ * @return txId The transaction ID for the pending request (use getTransaction(txId) for full record)
182
+ */
183
+ function updateBroadcasterRequest(address newBroadcaster, uint256 location) public returns (uint256 txId) {
184
+ SharedValidation.validateOwner(owner());
185
+ _requireNoPendingRequest(SecureOwnableDefinitions.BROADCASTER_UPDATE);
186
+ _requireNoPendingRequest(SecureOwnableDefinitions.OWNERSHIP_TRANSFER);
187
+
188
+ // Get the current broadcaster at the specified location. zero address if no broadcaster at location.
189
+ address currentBroadcaster = location < _getSecureState().roles[EngineBlox.BROADCASTER_ROLE].walletCount
190
+ ? _getAuthorizedWalletAt(EngineBlox.BROADCASTER_ROLE, location)
191
+ : address(0);
192
+
193
+ EngineBlox.TxRecord memory txRecord = _requestTransaction(
194
+ msg.sender,
195
+ address(this),
196
+ 0, // value
197
+ 0, // gas limit
198
+ SecureOwnableDefinitions.BROADCASTER_UPDATE,
199
+ SecureOwnableDefinitions.UPDATE_BROADCASTER_SELECTOR,
200
+ abi.encode(newBroadcaster, location)
201
+ );
202
+
203
+ _hasOpenBroadcasterRequest = true;
204
+ _logAddressPairEvent(currentBroadcaster, newBroadcaster);
205
+ return txRecord.txId;
206
+ }
207
+
208
+ /**
209
+ * @dev Approves a pending broadcaster update transaction after the release time
210
+ * @param txId The transaction ID
211
+ * @return The transaction ID
212
+ */
213
+ function updateBroadcasterDelayedApproval(uint256 txId) public returns (uint256) {
214
+ SharedValidation.validateOwner(owner());
215
+ return _completeApprove(_approveTransaction(txId));
216
+ }
217
+
218
+ /**
219
+ * @dev Approves a pending broadcaster update transaction using a meta-transaction
220
+ * @param metaTx The meta-transaction
221
+ * @return The transaction ID
222
+ */
223
+ function updateBroadcasterApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
224
+ _validateBroadcasterAndOwnerSigner(metaTx);
225
+ return _completeApprove(_approveTransactionWithMetaTx(metaTx));
226
+ }
227
+
228
+ /**
229
+ * @dev Cancels a pending broadcaster update transaction
230
+ * @param txId The transaction ID
231
+ * @return The transaction ID
232
+ */
233
+ function updateBroadcasterCancellation(uint256 txId) public returns (uint256) {
234
+ SharedValidation.validateOwner(owner());
235
+ return _completeCancel(_cancelTransaction(txId));
236
+ }
237
+
238
+ /**
239
+ * @dev Cancels a pending broadcaster update transaction using a meta-transaction
240
+ * @param metaTx The meta-transaction
241
+ * @return The transaction ID
242
+ */
243
+ function updateBroadcasterCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
244
+ _validateBroadcasterAndOwnerSigner(metaTx);
245
+ return _completeCancel(_cancelTransactionWithMetaTx(metaTx));
246
+ }
247
+
248
+ // Recovery Management
249
+
250
+ /**
251
+ * @dev Requests and approves a recovery address update using a meta-transaction (owner signs, broadcaster submits).
252
+ * @notice Does **not** revert when an ownership transfer is pending. A pending transfer continues to target
253
+ * the recovery address snapshotted at its request until executed or cancelled by **current** recovery.
254
+ * @param metaTx The meta-transaction
255
+ * @return The transaction ID
256
+ */
257
+ function updateRecoveryRequestAndApprove(
258
+ EngineBlox.MetaTransaction memory metaTx
259
+ ) public returns (uint256) {
260
+ _validateBroadcasterAndOwnerSigner(metaTx);
261
+ EngineBlox.TxRecord memory txRecord = _requestAndApproveTransaction(metaTx);
262
+ return txRecord.txId;
263
+ }
264
+
265
+ // TimeLock Management
266
+
267
+ /**
268
+ * @dev Requests and approves a time lock period update using a meta-transaction
269
+ * @param metaTx The meta-transaction
270
+ * @return The transaction ID
271
+ */
272
+ function updateTimeLockRequestAndApprove(
273
+ EngineBlox.MetaTransaction memory metaTx
274
+ ) public returns (uint256) {
275
+ _validateBroadcasterAndOwnerSigner(metaTx);
276
+ EngineBlox.TxRecord memory txRecord = _requestAndApproveTransaction(metaTx);
277
+ return txRecord.txId;
278
+ }
279
+
280
+ // Execution Functions
281
+ /**
282
+ * @dev External function that can only be called by the contract itself to execute ownership transfer.
283
+ * @param newOwner The new owner; for the OWNERSHIP_TRANSFER flow this is the recovery address encoded at
284
+ * request time (see `transferOwnershipRequest`), not necessarily `getRecovery()` at execution time.
285
+ */
286
+ function executeTransferOwnership(address newOwner) external {
287
+ _validateExecuteBySelf();
288
+ _transferOwnership(newOwner);
289
+ }
290
+
291
+ /**
292
+ * @dev External function that can only be called by the contract itself to execute broadcaster update
293
+ * @param newBroadcaster The new broadcaster address (zero address to revoke at location)
294
+ * @param location The index in the broadcaster role's authorized wallets set
295
+ */
296
+ function executeBroadcasterUpdate(address newBroadcaster, uint256 location) external {
297
+ _validateExecuteBySelf();
298
+ _updateBroadcaster(newBroadcaster, location);
299
+ }
300
+
301
+ /**
302
+ * @dev External function that can only be called by the contract itself to execute recovery update
303
+ * @param newRecoveryAddress The new recovery address
304
+ */
305
+ function executeRecoveryUpdate(address newRecoveryAddress) external {
306
+ _validateExecuteBySelf();
307
+ _updateRecoveryAddress(newRecoveryAddress);
308
+ }
309
+
310
+ /**
311
+ * @dev External function that can only be called by the contract itself to execute timelock update
312
+ * @param newTimeLockPeriodSec The new timelock period in seconds
313
+ */
314
+ function executeTimeLockUpdate(uint256 newTimeLockPeriodSec) external {
315
+ _validateExecuteBySelf();
316
+ _updateTimeLockPeriod(newTimeLockPeriodSec);
317
+ }
318
+
319
+ // ============ INTERNAL FUNCTIONS ============
320
+
321
+
322
+ /**
323
+ * @dev Validates that the caller is the broadcaster and that the meta-tx signer is the owner.
324
+ * @param metaTx The meta-transaction to validate
325
+ */
326
+ function _validateBroadcasterAndOwnerSigner(EngineBlox.MetaTransaction memory metaTx) internal view {
327
+ _validateBroadcaster(msg.sender);
328
+ SharedValidation.validateOwnerIsSigner(metaTx.params.signer, owner());
329
+ }
330
+
331
+ /**
332
+ * @dev Completes ownership/broadcaster flow after approval: clears the matching pending flag and returns txId.
333
+ * @param updatedRecord The updated transaction record from approval
334
+ * @return txId The transaction ID
335
+ */
336
+ function _completeApprove(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
337
+ _clearPendingFlagForOperation(updatedRecord.params.operationType);
338
+ return updatedRecord.txId;
339
+ }
340
+
341
+ /**
342
+ * @dev Completes ownership/broadcaster flow after cancellation: clears the matching pending flag and returns txId.
343
+ * @param updatedRecord The updated transaction record from cancellation
344
+ * @return txId The transaction ID
345
+ */
346
+ function _completeCancel(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
347
+ _clearPendingFlagForOperation(updatedRecord.params.operationType);
348
+ return updatedRecord.txId;
349
+ }
350
+
351
+ /**
352
+ * @dev Reverts if the pending flag for `requestOperationType` is already set (one lane per call).
353
+ * `OWNERSHIP_TRANSFER` checks only `_hasOpenOwnershipRequest` (a broadcaster update may still be pending).
354
+ * `BROADCASTER_UPDATE` checks only `_hasOpenBroadcasterRequest`. Callers that need both lanes idle
355
+ * (e.g. `updateBroadcasterRequest`) invoke this once per operation type.
356
+ * @param requestOperationType Lane to validate (`OWNERSHIP_TRANSFER` or `BROADCASTER_UPDATE`).
357
+ */
358
+ function _requireNoPendingRequest(bytes32 requestOperationType) internal view {
359
+ if (requestOperationType == SecureOwnableDefinitions.OWNERSHIP_TRANSFER) {
360
+ if (_hasOpenOwnershipRequest) revert SharedValidation.PendingSecureRequest();
361
+ } else if (requestOperationType == SecureOwnableDefinitions.BROADCASTER_UPDATE) {
362
+ if (_hasOpenBroadcasterRequest) revert SharedValidation.PendingSecureRequest();
363
+ } else {
364
+ revert();
365
+ }
366
+ }
367
+
368
+ /**
369
+ * @dev Clears the pending flag for a completed or cancelled secure op (approve/cancel paths).
370
+ * @param operationType The tx record's `operationType` (`OWNERSHIP_TRANSFER` or `BROADCASTER_UPDATE`).
371
+ */
372
+ function _clearPendingFlagForOperation(bytes32 operationType) private {
373
+ if (operationType == SecureOwnableDefinitions.OWNERSHIP_TRANSFER) {
374
+ _hasOpenOwnershipRequest = false;
375
+ } else if (operationType == SecureOwnableDefinitions.BROADCASTER_UPDATE) {
376
+ _hasOpenBroadcasterRequest = false;
377
+ } else {
378
+ revert();
379
+ }
380
+ }
381
+
382
+ /**
383
+ * @dev Transfers ownership of the contract
384
+ * @param newOwner The new owner of the contract
385
+ */
386
+ function _transferOwnership(address newOwner) internal virtual {
387
+ address oldOwner = owner();
388
+ _updateWallet(EngineBlox.OWNER_ROLE, newOwner, oldOwner);
389
+ _logAddressPairEvent(oldOwner, newOwner);
390
+ }
391
+
392
+ /**
393
+ * @dev Updates the broadcaster role at a specific index (location)
394
+ * @param newBroadcaster The new broadcaster address (zero address to revoke)
395
+ * @param location The index in the broadcaster role's authorized wallets set
396
+ *
397
+ * Logic:
398
+ * - If a broadcaster exists at `location` and `newBroadcaster` is non-zero,
399
+ * update that slot from old to new (role remains full).
400
+ * - If no broadcaster exists at `location` and `newBroadcaster` is non-zero,
401
+ * assign `newBroadcaster` to the broadcaster role (respecting maxWallets).
402
+ * - If `newBroadcaster` is the zero address and a broadcaster exists at `location`,
403
+ * revoke that broadcaster from the role.
404
+ */
405
+ function _updateBroadcaster(address newBroadcaster, uint256 location) internal virtual {
406
+ EngineBlox.Role storage role = _getSecureState().roles[EngineBlox.BROADCASTER_ROLE];
407
+
408
+ address oldBroadcaster;
409
+ uint256 length = role.walletCount;
410
+
411
+ if (location < length) {
412
+ oldBroadcaster = _getAuthorizedWalletAt(EngineBlox.BROADCASTER_ROLE, location);
413
+ } else {
414
+ oldBroadcaster = address(0);
415
+ }
416
+
417
+ // Case 1: Revoke existing broadcaster at location
418
+ if (newBroadcaster == address(0)) {
419
+ if (oldBroadcaster != address(0)) {
420
+ _revokeWallet(EngineBlox.BROADCASTER_ROLE, oldBroadcaster);
421
+ _logAddressPairEvent(oldBroadcaster, address(0));
422
+ }
423
+ return;
424
+ }
425
+
426
+ // Case 2: Update existing broadcaster at location
427
+ if (oldBroadcaster != address(0)) {
428
+ _updateWallet(EngineBlox.BROADCASTER_ROLE, newBroadcaster, oldBroadcaster);
429
+ _logAddressPairEvent(oldBroadcaster, newBroadcaster);
430
+ return;
431
+ }
432
+
433
+ // Case 3: No broadcaster at location, assign a new one (will respect maxWallets)
434
+ _assignWallet(EngineBlox.BROADCASTER_ROLE, newBroadcaster);
435
+ _logAddressPairEvent(address(0), newBroadcaster);
436
+ }
437
+
438
+ /**
439
+ * @dev Updates the recovery address
440
+ * @param newRecoveryAddress The new recovery address
441
+ */
442
+ function _updateRecoveryAddress(address newRecoveryAddress) internal virtual {
443
+ address oldRecovery = getRecovery();
444
+ _updateWallet(EngineBlox.RECOVERY_ROLE, newRecoveryAddress, oldRecovery);
445
+ _logAddressPairEvent(oldRecovery, newRecoveryAddress);
446
+ }
447
+
448
+ /**
449
+ * @dev Emits ComponentEvent with ABI-encoded (address, address) payload. Reused to reduce contract size.
450
+ * @param a First address
451
+ * @param b Second address
452
+ */
453
+ function _logAddressPairEvent(address a, address b) internal {
454
+ _logComponentEvent(abi.encode(a, b));
455
+ }
456
+ }