@bloxchain/contracts 1.0.0-alpha.2 → 1.0.0-alpha.21

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 (43) hide show
  1. package/README.md +7 -7
  2. package/abi/BaseStateMachine.abi.json +798 -753
  3. package/abi/EngineBlox.abi.json +566 -576
  4. package/abi/GuardController.abi.json +1546 -2095
  5. package/abi/GuardControllerDefinitions.abi.json +416 -0
  6. package/abi/IDefinition.abi.json +57 -47
  7. package/abi/RuntimeRBAC.abi.json +901 -959
  8. package/abi/RuntimeRBACDefinitions.abi.json +265 -81
  9. package/abi/SecureOwnable.abi.json +1522 -2581
  10. package/abi/SecureOwnableDefinitions.abi.json +174 -164
  11. package/components/README.md +8 -0
  12. package/core/access/RuntimeRBAC.sol +253 -270
  13. package/core/access/interface/IRuntimeRBAC.sol +55 -84
  14. package/core/access/lib/definitions/RuntimeRBACDefinitions.sol +97 -4
  15. package/core/base/BaseStateMachine.sol +198 -108
  16. package/core/base/interface/IBaseStateMachine.sol +153 -153
  17. package/core/execution/GuardController.sol +156 -131
  18. package/core/execution/interface/IGuardController.sol +146 -120
  19. package/core/execution/lib/definitions/GuardControllerDefinitions.sol +207 -45
  20. package/core/lib/EngineBlox.sol +2636 -2322
  21. package/{interfaces → core/lib/interfaces}/IDefinition.sol +49 -49
  22. package/{interfaces → core/lib/interfaces}/IEventForwarder.sol +5 -3
  23. package/{utils → core/lib/utils}/SharedValidation.sol +69 -22
  24. package/core/pattern/Account.sol +84 -0
  25. package/core/security/SecureOwnable.sol +180 -146
  26. package/core/security/interface/ISecureOwnable.sol +105 -104
  27. package/core/security/lib/definitions/SecureOwnableDefinitions.sol +818 -786
  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/abi/SimpleVaultDefinitions.abi.json +0 -269
  40. package/core/research/BloxchainWallet.sol +0 -306
  41. package/core/research/erc20-blox/ERC20Blox.sol +0 -140
  42. package/core/research/erc20-blox/lib/definitions/ERC20BloxDefinitions.sol +0 -185
  43. package/interfaces/IOnActionHook.sol +0 -79
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: MPL-2.0
2
- pragma solidity 0.8.33;
2
+ pragma solidity 0.8.35;
3
3
 
4
4
  // Contracts imports
5
5
  import "../base/BaseStateMachine.sol";
6
6
  import "./lib/definitions/SecureOwnableDefinitions.sol";
7
- import "../../interfaces/IDefinition.sol";
8
- import "../../utils/SharedValidation.sol";
7
+ import "../lib/interfaces/IDefinition.sol";
8
+ import "../lib/utils/SharedValidation.sol";
9
9
  import "./interface/ISecureOwnable.sol";
10
10
 
11
11
  /**
@@ -27,8 +27,28 @@ import "./interface/ISecureOwnable.sol";
27
27
  * Each operation follows a request -> approval workflow with appropriate time locks
28
28
  * and authorization checks. Operations can be cancelled within specific time windows.
29
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.
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.
32
52
  *
33
53
  * This contract focuses purely on security logic while leveraging the BaseStateMachine
34
54
  * for transaction management, meta-transactions, and state machine operations.
@@ -36,8 +56,14 @@ import "./interface/ISecureOwnable.sol";
36
56
  abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
37
57
  using SharedValidation for *;
38
58
 
39
- /// @dev True while any pending ownership transfer or broadcaster update request exists; blocks new requests until handled.
40
- bool private _hasOpenRequest;
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;
41
67
 
42
68
  /**
43
69
  * @notice Initializer to initialize SecureOwnable state
@@ -54,18 +80,15 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
54
80
  uint256 timeLockPeriodSec,
55
81
  address eventForwarder
56
82
  ) 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
-
83
+ _initializeBaseStateMachine(initialOwner, broadcaster, recovery, timeLockPeriodSec, eventForwarder);
84
+
62
85
  // Load SecureOwnable-specific definitions
63
86
  IDefinition.RolePermission memory secureOwnablePermissions = SecureOwnableDefinitions.getRolePermissions();
64
87
  _loadDefinitions(
65
88
  SecureOwnableDefinitions.getFunctionSchemas(),
66
89
  secureOwnablePermissions.roleHashes,
67
90
  secureOwnablePermissions.functionPermissions,
68
- true // Allow protected schemas for factory settings
91
+ true // Enforce all function schemas are protected
69
92
  );
70
93
  }
71
94
 
@@ -81,12 +104,14 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
81
104
 
82
105
  // Ownership Management
83
106
  /**
84
- * @dev Requests a transfer of ownership
85
- * @return The transaction record
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)
86
111
  */
87
- function transferOwnershipRequest() public returns (EngineBlox.TxRecord memory) {
112
+ function transferOwnershipRequest() public returns (uint256 txId) {
88
113
  SharedValidation.validateRecovery(getRecovery());
89
- _requireNoPendingRequest();
114
+ _requireNoPendingRequest(SecureOwnableDefinitions.OWNERSHIP_TRANSFER);
90
115
 
91
116
  EngineBlox.TxRecord memory txRecord = _requestTransaction(
92
117
  msg.sender,
@@ -98,66 +123,70 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
98
123
  abi.encode(getRecovery())
99
124
  );
100
125
 
101
- _hasOpenRequest = true;
102
- _logComponentEvent(abi.encode(owner(), getRecovery()));
103
- return txRecord;
126
+ _hasOpenOwnershipRequest = true;
127
+ _logAddressPairEvent(owner(), getRecovery());
128
+ return txRecord.txId;
104
129
  }
105
130
 
106
131
  /**
107
- * @dev Approves a pending ownership transfer transaction after the release time
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.
108
135
  * @param txId The transaction ID
109
- * @return The updated transaction record
136
+ * @return The transaction ID
110
137
  */
111
- function transferOwnershipDelayedApproval(uint256 txId) public returns (EngineBlox.TxRecord memory) {
138
+ function transferOwnershipDelayedApproval(uint256 txId) public returns (uint256) {
112
139
  SharedValidation.validateOwnerOrRecovery(owner(), getRecovery());
113
-
114
- return _completeOwnershipApprove(_approveTransaction(txId));
140
+ return _completeApprove(_approveTransaction(txId));
115
141
  }
116
142
 
117
143
  /**
118
144
  * @dev Approves a pending ownership transfer transaction using a meta-transaction
119
145
  * @param metaTx The meta-transaction
120
- * @return The updated transaction record
146
+ * @return The transaction ID
121
147
  */
122
- function transferOwnershipApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (EngineBlox.TxRecord memory) {
148
+ function transferOwnershipApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
123
149
  _validateBroadcasterAndOwnerSigner(metaTx);
124
-
125
- return _completeOwnershipApprove(_approveTransactionWithMetaTx(metaTx));
150
+ return _completeApprove(_approveTransactionWithMetaTx(metaTx));
126
151
  }
127
152
 
128
153
  /**
129
- * @dev Cancels a pending ownership transfer transaction
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.
130
157
  * @param txId The transaction ID
131
- * @return The updated transaction record
158
+ * @return The transaction ID
132
159
  */
133
- function transferOwnershipCancellation(uint256 txId) public returns (EngineBlox.TxRecord memory) {
160
+ function transferOwnershipCancellation(uint256 txId) public returns (uint256) {
134
161
  SharedValidation.validateRecovery(getRecovery());
135
- return _completeOwnershipCancel(_cancelTransaction(txId));
162
+ return _completeCancel(_cancelTransaction(txId));
136
163
  }
137
164
 
138
165
  /**
139
166
  * @dev Cancels a pending ownership transfer transaction using a meta-transaction
140
167
  * @param metaTx The meta-transaction
141
- * @return The updated transaction record
168
+ * @return The transaction ID
142
169
  */
143
- function transferOwnershipCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (EngineBlox.TxRecord memory) {
170
+ function transferOwnershipCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
144
171
  _validateBroadcasterAndOwnerSigner(metaTx);
145
-
146
- return _completeOwnershipCancel(_cancelTransactionWithMetaTx(metaTx));
172
+ return _completeCancel(_cancelTransactionWithMetaTx(metaTx));
147
173
  }
148
174
 
149
175
  // Broadcaster Management
150
176
  /**
151
- * @dev Updates the broadcaster address
152
- * @param newBroadcaster The new broadcaster address
153
- * @return The execution options
177
+ * @dev Requests a broadcaster role change identified by addresses.
178
+ * @notice Requires no pending broadcaster-update and no pending ownership-transfer request.
179
+ * @param newBroadcaster New broadcaster (`address(0)` to revoke `currentBroadcaster`)
180
+ * @param currentBroadcaster Existing broadcaster to replace or revoke; `address(0)` to add `newBroadcaster`
181
+ * @return txId The transaction ID for the pending request (use getTransaction(txId) for full record)
154
182
  */
155
- function updateBroadcasterRequest(address newBroadcaster) public returns (EngineBlox.TxRecord memory) {
183
+ function updateBroadcasterRequest(address newBroadcaster, address currentBroadcaster) public returns (uint256 txId) {
156
184
  SharedValidation.validateOwner(owner());
157
- _requireNoPendingRequest();
158
- address currentBroadcaster = _getAuthorizedWalletAt(EngineBlox.BROADCASTER_ROLE, 0);
159
- SharedValidation.validateAddressUpdate(newBroadcaster, currentBroadcaster);
160
-
185
+ _requireNoPendingRequest(SecureOwnableDefinitions.BROADCASTER_UPDATE);
186
+ _requireNoPendingRequest(SecureOwnableDefinitions.OWNERSHIP_TRANSFER);
187
+
188
+ _validateBroadcasterUpdatePair(newBroadcaster, currentBroadcaster);
189
+
161
190
  EngineBlox.TxRecord memory txRecord = _requestTransaction(
162
191
  msg.sender,
163
192
  address(this),
@@ -165,69 +194,69 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
165
194
  0, // gas limit
166
195
  SecureOwnableDefinitions.BROADCASTER_UPDATE,
167
196
  SecureOwnableDefinitions.UPDATE_BROADCASTER_SELECTOR,
168
- abi.encode(newBroadcaster)
197
+ abi.encode(newBroadcaster, currentBroadcaster)
169
198
  );
170
199
 
171
- _hasOpenRequest = true;
172
- _logComponentEvent(abi.encode(currentBroadcaster, newBroadcaster));
173
- return txRecord;
200
+ _hasOpenBroadcasterRequest = true;
201
+ _logAddressPairEvent(currentBroadcaster, newBroadcaster);
202
+ return txRecord.txId;
174
203
  }
175
204
 
176
205
  /**
177
206
  * @dev Approves a pending broadcaster update transaction after the release time
178
207
  * @param txId The transaction ID
179
- * @return The updated transaction record
208
+ * @return The transaction ID
180
209
  */
181
- function updateBroadcasterDelayedApproval(uint256 txId) public returns (EngineBlox.TxRecord memory) {
210
+ function updateBroadcasterDelayedApproval(uint256 txId) public returns (uint256) {
182
211
  SharedValidation.validateOwner(owner());
183
- return _completeBroadcasterApprove(_approveTransaction(txId));
212
+ return _completeApprove(_approveTransaction(txId));
184
213
  }
185
214
 
186
215
  /**
187
216
  * @dev Approves a pending broadcaster update transaction using a meta-transaction
188
217
  * @param metaTx The meta-transaction
189
- * @return The updated transaction record
218
+ * @return The transaction ID
190
219
  */
191
- function updateBroadcasterApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (EngineBlox.TxRecord memory) {
220
+ function updateBroadcasterApprovalWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
192
221
  _validateBroadcasterAndOwnerSigner(metaTx);
193
-
194
- return _completeBroadcasterApprove(_approveTransactionWithMetaTx(metaTx));
222
+ return _completeApprove(_approveTransactionWithMetaTx(metaTx));
195
223
  }
196
224
 
197
225
  /**
198
226
  * @dev Cancels a pending broadcaster update transaction
199
227
  * @param txId The transaction ID
200
- * @return The updated transaction record
228
+ * @return The transaction ID
201
229
  */
202
- function updateBroadcasterCancellation(uint256 txId) public returns (EngineBlox.TxRecord memory) {
230
+ function updateBroadcasterCancellation(uint256 txId) public returns (uint256) {
203
231
  SharedValidation.validateOwner(owner());
204
- return _completeBroadcasterCancel(_cancelTransaction(txId));
232
+ return _completeCancel(_cancelTransaction(txId));
205
233
  }
206
234
 
207
235
  /**
208
236
  * @dev Cancels a pending broadcaster update transaction using a meta-transaction
209
237
  * @param metaTx The meta-transaction
210
- * @return The updated transaction record
238
+ * @return The transaction ID
211
239
  */
212
- function updateBroadcasterCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (EngineBlox.TxRecord memory) {
240
+ function updateBroadcasterCancellationWithMetaTx(EngineBlox.MetaTransaction memory metaTx) public returns (uint256) {
213
241
  _validateBroadcasterAndOwnerSigner(metaTx);
214
-
215
- return _completeBroadcasterCancel(_cancelTransactionWithMetaTx(metaTx));
242
+ return _completeCancel(_cancelTransactionWithMetaTx(metaTx));
216
243
  }
217
244
 
218
245
  // Recovery Management
219
246
 
220
247
  /**
221
- * @dev Requests and approves a recovery address update using a meta-transaction
248
+ * @dev Requests and approves a recovery address update using a meta-transaction (owner signs, broadcaster submits).
249
+ * @notice Does **not** revert when an ownership transfer is pending. A pending transfer continues to target
250
+ * the recovery address snapshotted at its request until executed or cancelled by **current** recovery.
222
251
  * @param metaTx The meta-transaction
223
- * @return The transaction record
252
+ * @return The transaction ID
224
253
  */
225
254
  function updateRecoveryRequestAndApprove(
226
255
  EngineBlox.MetaTransaction memory metaTx
227
- ) public returns (EngineBlox.TxRecord memory) {
256
+ ) public returns (uint256) {
228
257
  _validateBroadcasterAndOwnerSigner(metaTx);
229
-
230
- return _requestAndApproveTransaction(metaTx);
258
+ EngineBlox.TxRecord memory txRecord = _requestAndApproveTransaction(metaTx);
259
+ return txRecord.txId;
231
260
  }
232
261
 
233
262
  // TimeLock Management
@@ -235,20 +264,21 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
235
264
  /**
236
265
  * @dev Requests and approves a time lock period update using a meta-transaction
237
266
  * @param metaTx The meta-transaction
238
- * @return The transaction record
267
+ * @return The transaction ID
239
268
  */
240
269
  function updateTimeLockRequestAndApprove(
241
270
  EngineBlox.MetaTransaction memory metaTx
242
- ) public returns (EngineBlox.TxRecord memory) {
271
+ ) public returns (uint256) {
243
272
  _validateBroadcasterAndOwnerSigner(metaTx);
244
-
245
- return _requestAndApproveTransaction(metaTx);
273
+ EngineBlox.TxRecord memory txRecord = _requestAndApproveTransaction(metaTx);
274
+ return txRecord.txId;
246
275
  }
247
276
 
248
277
  // Execution Functions
249
278
  /**
250
- * @dev External function that can only be called by the contract itself to execute ownership transfer
251
- * @param newOwner The new owner address
279
+ * @dev External function that can only be called by the contract itself to execute ownership transfer.
280
+ * @param newOwner The new owner; for the OWNERSHIP_TRANSFER flow this is the recovery address encoded at
281
+ * request time (see `transferOwnershipRequest`), not necessarily `getRecovery()` at execution time.
252
282
  */
253
283
  function executeTransferOwnership(address newOwner) external {
254
284
  _validateExecuteBySelf();
@@ -257,11 +287,12 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
257
287
 
258
288
  /**
259
289
  * @dev External function that can only be called by the contract itself to execute broadcaster update
260
- * @param newBroadcaster The new broadcaster address
290
+ * @param newBroadcaster New broadcaster (`address(0)` to revoke `currentBroadcaster`)
291
+ * @param currentBroadcaster Existing broadcaster to replace or revoke; `address(0)` to add `newBroadcaster`
261
292
  */
262
- function executeBroadcasterUpdate(address newBroadcaster) external {
293
+ function executeBroadcasterUpdate(address newBroadcaster, address currentBroadcaster) external {
263
294
  _validateExecuteBySelf();
264
- _updateBroadcaster(newBroadcaster, 0);
295
+ _updateBroadcaster(newBroadcaster, currentBroadcaster);
265
296
  }
266
297
 
267
298
  /**
@@ -284,12 +315,6 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
284
315
 
285
316
  // ============ INTERNAL FUNCTIONS ============
286
317
 
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
318
 
294
319
  /**
295
320
  * @dev Validates that the caller is the broadcaster and that the meta-tx signer is the owner.
@@ -301,37 +326,54 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
301
326
  }
302
327
 
303
328
  /**
304
- * @dev Completes ownership flow after approval: resets flag and returns record.
329
+ * @dev Completes ownership/broadcaster flow after approval: clears the matching pending flag and returns txId.
330
+ * @param updatedRecord The updated transaction record from approval
331
+ * @return txId The transaction ID
305
332
  */
306
- function _completeOwnershipApprove(EngineBlox.TxRecord memory updatedRecord) internal returns (EngineBlox.TxRecord memory) {
307
- _hasOpenRequest = false;
308
- return updatedRecord;
333
+ function _completeApprove(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
334
+ _clearPendingFlagForOperation(updatedRecord.params.operationType);
335
+ return updatedRecord.txId;
309
336
  }
310
337
 
311
338
  /**
312
- * @dev Completes ownership flow after cancellation: resets flag, logs txId, returns record.
339
+ * @dev Completes ownership/broadcaster flow after cancellation: clears the matching pending flag and returns txId.
340
+ * @param updatedRecord The updated transaction record from cancellation
341
+ * @return txId The transaction ID
313
342
  */
314
- function _completeOwnershipCancel(EngineBlox.TxRecord memory updatedRecord) internal returns (EngineBlox.TxRecord memory) {
315
- _hasOpenRequest = false;
316
- _logComponentEvent(abi.encode(updatedRecord.txId));
317
- return updatedRecord;
343
+ function _completeCancel(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
344
+ _clearPendingFlagForOperation(updatedRecord.params.operationType);
345
+ return updatedRecord.txId;
318
346
  }
319
347
 
320
348
  /**
321
- * @dev Completes broadcaster flow after approval: resets flag and returns record.
349
+ * @dev Reverts if the pending flag for `requestOperationType` is already set (one lane per call).
350
+ * `OWNERSHIP_TRANSFER` checks only `_hasOpenOwnershipRequest` (a broadcaster update may still be pending).
351
+ * `BROADCASTER_UPDATE` checks only `_hasOpenBroadcasterRequest`. Callers that need both lanes idle
352
+ * (e.g. `updateBroadcasterRequest`) invoke this once per operation type.
353
+ * @param requestOperationType Lane to validate (`OWNERSHIP_TRANSFER` or `BROADCASTER_UPDATE`).
322
354
  */
323
- function _completeBroadcasterApprove(EngineBlox.TxRecord memory updatedRecord) internal returns (EngineBlox.TxRecord memory) {
324
- _hasOpenRequest = false;
325
- return updatedRecord;
355
+ function _requireNoPendingRequest(bytes32 requestOperationType) internal view {
356
+ if (requestOperationType == SecureOwnableDefinitions.OWNERSHIP_TRANSFER) {
357
+ if (_hasOpenOwnershipRequest) revert SharedValidation.PendingSecureRequest();
358
+ } else if (requestOperationType == SecureOwnableDefinitions.BROADCASTER_UPDATE) {
359
+ if (_hasOpenBroadcasterRequest) revert SharedValidation.PendingSecureRequest();
360
+ } else {
361
+ revert();
362
+ }
326
363
  }
327
364
 
328
365
  /**
329
- * @dev Completes broadcaster flow after cancellation: resets flag, logs txId, returns record.
366
+ * @dev Clears the pending flag for a completed or cancelled secure op (approve/cancel paths).
367
+ * @param operationType The tx record's `operationType` (`OWNERSHIP_TRANSFER` or `BROADCASTER_UPDATE`).
330
368
  */
331
- function _completeBroadcasterCancel(EngineBlox.TxRecord memory updatedRecord) internal returns (EngineBlox.TxRecord memory) {
332
- _hasOpenRequest = false;
333
- _logComponentEvent(abi.encode(updatedRecord.txId));
334
- return updatedRecord;
369
+ function _clearPendingFlagForOperation(bytes32 operationType) private {
370
+ if (operationType == SecureOwnableDefinitions.OWNERSHIP_TRANSFER) {
371
+ _hasOpenOwnershipRequest = false;
372
+ } else if (operationType == SecureOwnableDefinitions.BROADCASTER_UPDATE) {
373
+ _hasOpenBroadcasterRequest = false;
374
+ } else {
375
+ revert();
376
+ }
335
377
  }
336
378
 
337
379
  /**
@@ -340,54 +382,47 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
340
382
  */
341
383
  function _transferOwnership(address newOwner) internal virtual {
342
384
  address oldOwner = owner();
343
- _updateAssignedWallet(EngineBlox.OWNER_ROLE, newOwner, oldOwner);
344
- _logComponentEvent(abi.encode(oldOwner, newOwner));
385
+ _updateWallet(EngineBlox.OWNER_ROLE, newOwner, oldOwner);
386
+ _logAddressPairEvent(oldOwner, newOwner);
345
387
  }
346
388
 
347
389
  /**
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.
390
+ * @dev Validates broadcaster update pair at request time.
391
+ * @param newBroadcaster New broadcaster (`address(0)` to revoke)
392
+ * @param currentBroadcaster Existing broadcaster; `address(0)` to add `newBroadcaster`
359
393
  */
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);
394
+ function _validateBroadcasterUpdatePair(address newBroadcaster, address currentBroadcaster) internal view {
395
+ if (newBroadcaster == address(0) && currentBroadcaster == address(0)) {
396
+ revert SharedValidation.InvalidOperation(address(0));
370
397
  }
398
+ bytes32 role = EngineBlox.BROADCASTER_ROLE;
399
+ if (currentBroadcaster != address(0)) {
400
+ if (!hasRole(role, currentBroadcaster)) revert SharedValidation.ItemNotFound(currentBroadcaster);
401
+ }
402
+ if (newBroadcaster != address(0)) {
403
+ if (hasRole(role, newBroadcaster)) revert SharedValidation.ItemAlreadyExists(newBroadcaster);
404
+ }
405
+ }
371
406
 
372
- // Case 1: Revoke existing broadcaster at location
407
+ /**
408
+ * @dev Updates the broadcaster role by address pair (revoke, replace, or add).
409
+ * @param newBroadcaster New broadcaster (`address(0)` to revoke `currentBroadcaster`)
410
+ * @param currentBroadcaster Existing broadcaster; `address(0)` to add `newBroadcaster`
411
+ */
412
+ function _updateBroadcaster(address newBroadcaster, address currentBroadcaster) internal virtual {
413
+ bytes32 role = EngineBlox.BROADCASTER_ROLE;
373
414
  if (newBroadcaster == address(0)) {
374
- if (oldBroadcaster != address(0)) {
375
- _revokeWallet(EngineBlox.BROADCASTER_ROLE, oldBroadcaster);
376
- _logComponentEvent(abi.encode(oldBroadcaster, address(0)));
377
- }
415
+ _revokeWallet(role, currentBroadcaster);
416
+ _logAddressPairEvent(currentBroadcaster, address(0));
378
417
  return;
379
418
  }
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));
419
+ if (currentBroadcaster == address(0)) {
420
+ _assignWallet(role, newBroadcaster);
421
+ _logAddressPairEvent(address(0), newBroadcaster);
385
422
  return;
386
423
  }
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));
424
+ _updateWallet(role, newBroadcaster, currentBroadcaster);
425
+ _logAddressPairEvent(currentBroadcaster, newBroadcaster);
391
426
  }
392
427
 
393
428
  /**
@@ -396,17 +431,16 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
396
431
  */
397
432
  function _updateRecoveryAddress(address newRecoveryAddress) internal virtual {
398
433
  address oldRecovery = getRecovery();
399
- _updateAssignedWallet(EngineBlox.RECOVERY_ROLE, newRecoveryAddress, oldRecovery);
400
- _logComponentEvent(abi.encode(oldRecovery, newRecoveryAddress));
434
+ _updateWallet(EngineBlox.RECOVERY_ROLE, newRecoveryAddress, oldRecovery);
435
+ _logAddressPairEvent(oldRecovery, newRecoveryAddress);
401
436
  }
402
437
 
403
438
  /**
404
- * @dev Updates the time lock period
405
- * @param newTimeLockPeriodSec The new time lock period in seconds
439
+ * @dev Emits ComponentEvent with ABI-encoded (address, address) payload. Reused to reduce contract size.
440
+ * @param a First address
441
+ * @param b Second address
406
442
  */
407
- function _updateTimeLockPeriod(uint256 newTimeLockPeriodSec) internal virtual override {
408
- uint256 oldPeriod = getTimeLockPeriodSec();
409
- super._updateTimeLockPeriod(newTimeLockPeriodSec);
410
- _logComponentEvent(abi.encode(oldPeriod, newTimeLockPeriodSec));
443
+ function _logAddressPairEvent(address a, address b) internal {
444
+ _logComponentEvent(abi.encode(a, b));
411
445
  }
412
446
  }