@@ -27,17 +27,20 @@ import "./interface/ISecureOwnable.sol";
2727 * Each operation follows a request -> approval workflow with appropriate time locks
2828 * and authorization checks. Operations can be cancelled within specific time windows.
2929 *
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.
3234 *
3335 * This contract focuses purely on security logic while leveraging the BaseStateMachine
3436 * for transaction management, meta-transactions, and state machine operations.
3537 */
3638abstract contract SecureOwnable is BaseStateMachine , ISecureOwnable {
3739 using SharedValidation for * ;
3840
39- /// @dev True while any pending ownership transfer or broadcaster update request exists; blocks new requests until handled.
40- bool private _hasOpenRequest;
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;
4144
4245 /**
4346 * @notice Initializer to initialize SecureOwnable state
@@ -83,7 +86,7 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
8386 */
8487 function transferOwnershipRequest () public returns (uint256 txId ) {
8588 SharedValidation.validateRecovery (getRecovery ());
86- _requireNoPendingRequest ();
89+ _requireNoPendingRequest (SecureOwnableDefinitions.OWNERSHIP_TRANSFER );
8790
8891 EngineBlox.TxRecord memory txRecord = _requestTransaction (
8992 msg .sender ,
@@ -95,7 +98,7 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
9598 abi.encode (getRecovery ())
9699 );
97100
98- _hasOpenRequest = true ;
101+ _hasOpenOwnershipRequest = true ;
99102 _logAddressPairEvent (owner (), getRecovery ());
100103 return txRecord.txId;
101104 }
@@ -143,13 +146,15 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
143146 // Broadcaster Management
144147 /**
145148 * @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.
146150 * @param newBroadcaster The new broadcaster address (zero address to revoke at location)
147151 * @param location The index in the broadcaster role's authorized wallets set
148152 * @return txId The transaction ID for the pending request (use getTransaction(txId) for full record)
149153 */
150154 function updateBroadcasterRequest (address newBroadcaster , uint256 location ) public returns (uint256 txId ) {
151155 SharedValidation.validateOwner (owner ());
152- _requireNoPendingRequest ();
156+ _requireNoPendingRequest (SecureOwnableDefinitions.BROADCASTER_UPDATE);
157+ _requireNoPendingRequest (SecureOwnableDefinitions.OWNERSHIP_TRANSFER);
153158
154159 // Get the current broadcaster at the specified location. zero address if no broadcaster at location.
155160 address currentBroadcaster = location < _getSecureState ().roles[EngineBlox.BROADCASTER_ROLE].walletCount
@@ -166,7 +171,7 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
166171 abi.encode (newBroadcaster, location)
167172 );
168173
169- _hasOpenRequest = true ;
174+ _hasOpenBroadcasterRequest = true ;
170175 _logAddressPairEvent (currentBroadcaster, newBroadcaster);
171176 return txRecord.txId;
172177 }
@@ -281,12 +286,6 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
281286
282287 // ============ INTERNAL FUNCTIONS ============
283288
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- }
290289
291290 /**
292291 * @dev Validates that the caller is the broadcaster and that the meta-tx signer is the owner.
@@ -298,25 +297,56 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
298297 }
299298
300299 /**
301- * @dev Completes ownership/broadcaster flow after approval: resets flag and returns txId.
300+ * @dev Completes ownership/broadcaster flow after approval: clears the matching pending flag and returns txId.
302301 * @param updatedRecord The updated transaction record from approval
303302 * @return txId The transaction ID
304303 */
305304 function _completeApprove (EngineBlox.TxRecord memory updatedRecord ) internal returns (uint256 txId ) {
306- _hasOpenRequest = false ;
305+ _clearPendingFlagForOperation (updatedRecord.params.operationType) ;
307306 return updatedRecord.txId;
308307 }
309308
310309 /**
311- * @dev Completes ownership/broadcaster flow after cancellation: resets flag, logs txId, returns txId.
310+ * @dev Completes ownership/broadcaster flow after cancellation: clears the matching pending flag and returns txId.
312311 * @param updatedRecord The updated transaction record from cancellation
313312 * @return txId The transaction ID
314313 */
315314 function _completeCancel (EngineBlox.TxRecord memory updatedRecord ) internal returns (uint256 txId ) {
316- _hasOpenRequest = false ;
315+ _clearPendingFlagForOperation (updatedRecord.params.operationType) ;
317316 return updatedRecord.txId;
318317 }
319318
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+
320350 /**
321351 * @dev Transfers ownership of the contract
322352 * @param newOwner The new owner of the contract
0 commit comments