2222 multicall_contract ,
2323 os_token_redeemer_contract ,
2424)
25- from src .common .execution import check_gas_price , transaction_gas_wrapper
25+ from src .common .execution import (
26+ check_gas_price ,
27+ transaction_gas_wrapper ,
28+ wait_for_execution_endpoints_synced ,
29+ )
2630from src .common .harvest import get_multiple_harvest_params
2731from src .common .logging import LOG_LEVELS , setup_logging
2832from src .common .utils import log_verbose
@@ -196,17 +200,31 @@ async def process(
196200 block_number : BlockNumber ,
197201 min_queued_assets : Gwei ,
198202) -> None :
203+ try :
204+ await _redeem_os_token_positions (min_queued_assets = min_queued_assets )
205+ finally :
206+ # Re-fetch block number after redemption processing
207+ # to ensure we read the latest on-chain state.
208+ block_number = await execution_client .eth .block_number
209+ await _process_exit_queue (block_number )
210+
211+
212+ async def _redeem_os_token_positions (
213+ min_queued_assets : Gwei ,
214+ ) -> None :
215+ """Perform the OsToken redemption flow for a single iteration.
216+
217+ Returns early without raising when there is nothing to redeem; the caller
218+ is responsible for processing the exit queue regardless of the outcome.
219+ """
199220 if not await check_gas_price ():
200221 return
201222
202- # Step 1: Process exit queue
203- await _process_exit_queue (block_number )
204-
205223 # Re-fetch block number after exit queue processing
206224 # to ensure we read the latest on-chain state
207225 block_number = await execution_client .eth .block_number
208226
209- # Step 2: Check queued shares
227+ # Check queued shares
210228 queued_shares = await os_token_redeemer_contract .queued_shares (block_number )
211229 os_token_converter = await create_os_token_converter (block_number )
212230 queued_assets = os_token_converter .to_assets (queued_shares )
@@ -233,25 +251,25 @@ async def process(
233251 settings .network_config .VAULT_BALANCE_SYMBOL ,
234252 )
235253
236- # Step 3: Fetch ALL positions from IPFS (needed for correct merkle tree)
254+ # Fetch ALL positions from IPFS (needed for correct merkle tree)
237255 all_positions = await fetch_positions_from_ipfs (block_number )
238256 if not all_positions :
239257 logger .info ('No positions found. Skipping to next interval.' )
240258 return
241259
242- # Step 4: Calculate redeemable shares
260+ # Calculate redeemable shares
243261 os_token_positions = await calculate_redeemable_shares (all_positions , prev_nonce , block_number )
244262 if not os_token_positions :
245263 logger .info ('No redeemable positions found. Skipping to next interval.' )
246264 return
247265
248- # Step 5: Bring every involved vault up to date on-chain so subsequent reads
266+ # Bring every involved vault up to date on-chain so subsequent reads
249267 # and redemption transactions run against fresh state — no harvest_params
250268 # plumbing and no updateVaultState bundled in a multicall.
251269 vaults = list ({position .vault for position in os_token_positions })
252270 await update_vaults_state (vaults = vaults , block_number = block_number )
253271
254- # Step 6: Redeem each position end-to-end in a single loop. Withdrawable assets
272+ # Redeem each position end-to-end in a single loop. Withdrawable assets
255273 # are fetched once per vault and cached; meta vaults short on assets trigger
256274 # sub-vault redemption inline; one redeemOsTokenPositions tx is submitted per
257275 # position.
@@ -408,13 +426,16 @@ async def redeem_positions(
408426 continue
409427
410428 position_to_redeem = replace (position , shares_to_redeem = shares_to_redeem )
411- if not await _submit_redeem_position (
429+ receipt_block = await _submit_redeem_position (
412430 position = position_to_redeem ,
413431 all_positions = all_positions ,
414432 tree_nonce = tree_nonce ,
415- ):
433+ )
434+ if receipt_block is None :
416435 return
417436
437+ await wait_for_execution_endpoints_synced (receipt_block )
438+
418439 vault_to_withdrawable [position .vault ] = Wei (withdrawable - assets_to_redeem )
419440 remaining_shares -= shares_to_redeem
420441
@@ -513,8 +534,14 @@ async def _submit_redeem_position(
513534 position : OsTokenPosition ,
514535 all_positions : list [OsTokenPosition ],
515536 tree_nonce : int ,
516- ) -> bool :
517- """Submit one redeemOsTokenPositions transaction for a single position."""
537+ ) -> BlockNumber | None :
538+ """Submit one redeemOsTokenPositions transaction for a single position.
539+
540+ Returns the receipt's block number on a confirmed successful transaction,
541+ ``None`` otherwise. The caller uses the returned block as a sync barrier so
542+ that subsequent reads (e.g. withdrawable balances for the next position)
543+ cannot land on a fallback endpoint that has not yet seen the redemption.
544+ """
518545 multiproof = _build_multi_proof (
519546 tree_nonce = tree_nonce ,
520547 all_positions = all_positions ,
@@ -534,7 +561,7 @@ async def _submit_redeem_position(
534561 position .vault ,
535562 position .owner ,
536563 )
537- return False
564+ return None
538565
539566 tx_hash = Web3 .to_hex (tx )
540567 logger .info (
@@ -553,7 +580,7 @@ async def _submit_redeem_position(
553580 position .owner ,
554581 tx_hash ,
555582 )
556- return False
583+ return None
557584
558585 logger .info (
559586 'Redeemed %s shares for position (vault %s, owner %s). Tx Hash: %s' ,
@@ -562,7 +589,7 @@ async def _submit_redeem_position(
562589 position .owner ,
563590 tx_hash ,
564591 )
565- return True
592+ return tx_receipt [ 'blockNumber' ]
566593
567594
568595def _build_multi_proof (
0 commit comments