Skip to content

Commit 3e10f31

Browse files
committed
Skip deposit to sub vaults
Signed-off-by: cyc60 <avsysoev60@gmail.com>
1 parent ece992d commit 3e10f31

2 files changed

Lines changed: 94 additions & 55 deletions

File tree

src/commands/internal/process_redeemer.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
from src.common.wallet import wallet
3434
from src.config.networks import AVAILABLE_NETWORKS, ZERO_CHECKSUM_ADDRESS
3535
from src.config.settings import settings
36+
from src.meta_vault.exceptions import ClaimDelayNotPassedException
3637
from src.meta_vault.graph import graph_get_vaults
3738
from src.meta_vault.service import is_meta_vault, is_meta_vault_state_update_required
38-
from src.meta_vault.tasks import process_meta_vault_tree
39+
from src.meta_vault.tasks import meta_vault_tree_update_state
3940
from src.meta_vault.typings import SubVaultRedemption
4041
from src.redemptions.os_token_converter import (
4142
OsTokenConverter,
@@ -337,17 +338,35 @@ async def update_vaults_state(
337338
is_meta_vault=True,
338339
)
339340
for vault in vaults:
340-
if await is_meta_vault(vault):
341-
if await is_meta_vault_state_update_required(vault):
342-
try:
343-
await process_meta_vault_tree(vault=vault, meta_vaults_map=meta_vaults_map)
344-
except Exception as e:
345-
raise RuntimeError(
346-
f'Failed to process meta vault tree for vault {vault}'
347-
) from e
348-
349-
else:
341+
if vault not in meta_vaults_map:
350342
regular_vaults.append(vault)
343+
continue
344+
345+
if not await is_meta_vault_state_update_required(vault):
346+
continue
347+
348+
root_meta_vault = meta_vaults_map[vault]
349+
if not root_meta_vault.sub_vaults:
350+
continue
351+
352+
# Update state for the meta vault tree only — skip the deposit-to-sub-vaults
353+
# step that process_meta_vault_tree would otherwise perform. Pushing the meta
354+
# vault's withdrawable assets back into sub-vaults here would force a follow-up
355+
# redeemSubVaultsAssets to pull them right back up before redemption.
356+
try:
357+
await meta_vault_tree_update_state(
358+
root_meta_vault=root_meta_vault,
359+
meta_vaults_map=meta_vaults_map,
360+
)
361+
except ClaimDelayNotPassedException as e:
362+
logger.error(
363+
'Cannot update meta vault %s state because claim delay for exit request '
364+
'with position ticket %s has not passed yet',
365+
vault,
366+
e.exit_request.position_ticket,
367+
)
368+
except Exception as e:
369+
raise RuntimeError(f'Failed to update meta vault tree state for vault {vault}') from e
351370

352371
if not regular_vaults:
353372
return

src/commands/tests/test_internal/test_process_redeemer.py

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
update_vaults_state,
2626
)
2727
from src.common.typings import HarvestParams
28+
from src.meta_vault.exceptions import ClaimDelayNotPassedException
2829
from src.meta_vault.typings import SubVaultRedemption
2930
from src.redemptions.os_token_converter import OsTokenConverter
3031
from src.redemptions.typings import OsTokenPosition
@@ -462,41 +463,72 @@ class TestUpdateVaultsState:
462463
async def test_no_vaults(self) -> None:
463464
with _mock_update_vaults_state() as mocks:
464465
await update_vaults_state(vaults=[], block_number=BlockNumber(100))
465-
mocks['is_meta_vault'].assert_not_called()
466+
mocks['update_state'].assert_not_called()
466467
mocks['harvest_params'].assert_not_called()
467468

468469
@pytest.mark.parametrize(
469470
'needs_update, expected_calls',
470471
[(True, 1), (False, 0)],
471472
ids=['needs_update', 'up_to_date'],
472473
)
473-
async def test_meta_vault_process_tree_gated_by_needs_update(
474+
async def test_meta_vault_update_state_gated_by_needs_update(
474475
self, needs_update: bool, expected_calls: int
475476
) -> None:
476-
"""process_meta_vault_tree runs iff is_meta_vault_state_update_required is True.
477-
When it does, graph_get_vaults' result is forwarded by identity."""
478-
meta_vaults_map = {VAULT_1: MagicMock()}
477+
"""meta_vault_tree_update_state runs iff is_meta_vault_state_update_required is True.
478+
When it does, the corresponding Vault entry from meta_vaults_map is forwarded
479+
by identity along with the full meta_vaults_map."""
480+
root_meta_vault = MagicMock()
481+
meta_vaults_map = {VAULT_1: root_meta_vault}
479482
with _mock_update_vaults_state(
480-
is_meta_vault=True,
481483
needs_update=needs_update,
482484
meta_vaults_map=meta_vaults_map,
483485
) as mocks:
484486
await update_vaults_state(vaults=[VAULT_1], block_number=BlockNumber(100))
485487

486-
assert mocks['process_tree'].await_count == expected_calls
488+
assert mocks['update_state'].await_count == expected_calls
487489
if needs_update:
488490
mocks['graph_get_vaults'].assert_awaited_once_with(is_meta_vault=True)
489-
assert mocks['process_tree'].await_args.kwargs['vault'] == VAULT_1
490-
assert mocks['process_tree'].await_args.kwargs['meta_vaults_map'] is meta_vaults_map
491+
assert mocks['update_state'].await_args.kwargs['root_meta_vault'] is root_meta_vault
492+
assert mocks['update_state'].await_args.kwargs['meta_vaults_map'] is meta_vaults_map
493+
494+
async def test_meta_vault_no_sub_vaults_skipped(self) -> None:
495+
"""A meta vault with no sub-vaults is skipped — nothing to update."""
496+
empty_meta_vault = MagicMock()
497+
empty_meta_vault.sub_vaults = []
498+
with _mock_update_vaults_state(
499+
meta_vaults_map={VAULT_1: empty_meta_vault},
500+
) as mocks:
501+
await update_vaults_state(vaults=[VAULT_1], block_number=BlockNumber(100))
502+
mocks['update_state'].assert_not_called()
491503

492-
async def test_meta_vault_process_tree_failure_raises(self) -> None:
493-
"""A failure inside process_meta_vault_tree aborts the round."""
504+
async def test_meta_vault_update_state_failure_raises(self) -> None:
505+
"""A failure inside meta_vault_tree_update_state aborts the round."""
494506
with _mock_update_vaults_state(
495-
is_meta_vault=True, process_tree_exception=RuntimeError('boom')
507+
meta_vaults_map={VAULT_1: MagicMock()},
508+
update_state_exception=RuntimeError('boom'),
496509
):
497-
with pytest.raises(RuntimeError, match='Failed to process meta vault tree'):
510+
with pytest.raises(RuntimeError, match='Failed to update meta vault tree state'):
498511
await update_vaults_state(vaults=[VAULT_1], block_number=BlockNumber(100))
499512

513+
async def test_meta_vault_claim_delay_logged_and_continues(self) -> None:
514+
"""ClaimDelayNotPassedException is caught, logged, and does not abort the round.
515+
516+
Regular vaults batched alongside the meta vault are still processed.
517+
"""
518+
exit_request = MagicMock()
519+
exit_request.vault = VAULT_1
520+
exit_request.position_ticket = 1234
521+
with _mock_update_vaults_state(
522+
meta_vaults_map={VAULT_1: MagicMock()},
523+
harvest_params={VAULT_2: make_harvest_params()},
524+
update_state_exception=ClaimDelayNotPassedException(exit_request),
525+
) as mocks:
526+
await update_vaults_state(vaults=[VAULT_1, VAULT_2], block_number=BlockNumber(100))
527+
mocks['update_state'].assert_awaited_once()
528+
mocks['multicall'].tx_aggregate.assert_awaited_once_with(
529+
[(VAULT_2, ENCODED_UPDATE_STATE_CALL)]
530+
)
531+
500532
@pytest.mark.parametrize(
501533
'has_params, expected_multicall_calls',
502534
[(True, 1), (False, 0)],
@@ -507,9 +539,7 @@ async def test_regular_vault_multicall_gated_by_harvest_params(
507539
) -> None:
508540
"""A None entry in get_multiple_harvest_params skips that vault from the multicall."""
509541
params: HarvestParams | None = make_harvest_params() if has_params else None
510-
with _mock_update_vaults_state(
511-
is_meta_vault=False, harvest_params={VAULT_1: params}
512-
) as mocks:
542+
with _mock_update_vaults_state(harvest_params={VAULT_1: params}) as mocks:
513543
await update_vaults_state(vaults=[VAULT_1], block_number=BlockNumber(100))
514544

515545
assert mocks['multicall'].tx_aggregate.await_count == expected_multicall_calls
@@ -521,26 +551,28 @@ async def test_regular_vault_multicall_gated_by_harvest_params(
521551
async def test_multicall_tx_failure_raises(self) -> None:
522552
"""A failed multicall receipt aborts the round."""
523553
with _mock_update_vaults_state(
524-
is_meta_vault=False,
525554
harvest_params={VAULT_1: make_harvest_params()},
526555
multicall_tx_status=0,
527556
):
528557
with pytest.raises(RuntimeError, match='Update State multicall tx failed'):
529558
await update_vaults_state(vaults=[VAULT_1], block_number=BlockNumber(100))
530559

531560
async def test_mix_of_meta_and_regular_vaults(self) -> None:
532-
"""Meta vault is harvested via process_meta_vault_tree; regular vaults are
561+
"""Meta vault is harvested via meta_vault_tree_update_state; regular vaults are
533562
batched into a single multicall. Harvest params are fetched only for the
534563
regular vaults."""
535564
params = make_harvest_params()
565+
meta_vaults_map = {VAULT_1: MagicMock()}
536566
with _mock_update_vaults_state(
537-
is_meta_vault={VAULT_1: True, VAULT_2: False},
567+
meta_vaults_map=meta_vaults_map,
538568
harvest_params={VAULT_2: params},
539569
) as mocks:
540570
await update_vaults_state(vaults=[VAULT_1, VAULT_2], block_number=BlockNumber(100))
541571

542-
mocks['process_tree'].assert_awaited_once()
543-
assert mocks['process_tree'].await_args.kwargs['vault'] == VAULT_1
572+
mocks['update_state'].assert_awaited_once()
573+
assert (
574+
mocks['update_state'].await_args.kwargs['root_meta_vault'] is meta_vaults_map[VAULT_1]
575+
)
544576
mocks['harvest_params'].assert_awaited_once_with([VAULT_2], BlockNumber(100))
545577
mocks['multicall'].tx_aggregate.assert_awaited_once_with(
546578
[(VAULT_2, ENCODED_UPDATE_STATE_CALL)]
@@ -960,37 +992,27 @@ def _mock_submit_redeem_position(
960992

961993
@contextmanager
962994
def _mock_update_vaults_state(
963-
is_meta_vault: bool | dict[ChecksumAddress, bool] = False,
964995
needs_update: bool = True,
965996
harvest_params: dict[ChecksumAddress, HarvestParams | None] | None = None,
966997
meta_vaults_map: dict | None = None,
967-
process_tree_exception: BaseException | None = None,
998+
update_state_exception: BaseException | None = None,
968999
multicall_tx_status: int = 1,
9691000
) -> Iterator[dict[str, MagicMock]]:
9701001
"""Mock setup for update_vaults_state tests.
9711002
972-
``is_meta_vault`` may be a bool (applied to every address) or a per-address
973-
mapping. ``harvest_params`` is the dict returned by get_multiple_harvest_params;
974-
a None value for a vault skips it from the multicall (production behavior).
975-
``process_tree_exception`` makes process_meta_vault_tree raise.
1003+
``meta_vaults_map`` is the dict returned by graph_get_vaults; addresses present
1004+
in this map are treated as meta vaults by update_vaults_state. ``harvest_params``
1005+
is the dict returned by get_multiple_harvest_params; a None value for a vault
1006+
skips it from the multicall (production behavior). ``update_state_exception``
1007+
makes meta_vault_tree_update_state raise.
9761008
"""
9771009
meta_vaults_map = {} if meta_vaults_map is None else meta_vaults_map
9781010
harvest_params = {} if harvest_params is None else harvest_params
9791011

980-
if isinstance(is_meta_vault, dict):
981-
is_meta_lookup = is_meta_vault
982-
983-
async def is_meta_side_effect(addr: ChecksumAddress) -> bool:
984-
return is_meta_lookup.get(addr, False)
985-
986-
is_meta_mock: AsyncMock = AsyncMock(side_effect=is_meta_side_effect)
987-
else:
988-
is_meta_mock = AsyncMock(return_value=is_meta_vault)
989-
990-
if process_tree_exception is not None:
991-
process_tree_mock = AsyncMock(side_effect=process_tree_exception)
1012+
if update_state_exception is not None:
1013+
update_state_mock = AsyncMock(side_effect=update_state_exception)
9921014
else:
993-
process_tree_mock = AsyncMock()
1015+
update_state_mock = AsyncMock()
9941016

9951017
def vault_factory(addr: ChecksumAddress) -> MagicMock:
9961018
mock_vault = MagicMock()
@@ -1003,12 +1025,11 @@ def vault_factory(addr: ChecksumAddress) -> MagicMock:
10031025
f'{MODULE}.graph_get_vaults',
10041026
new=AsyncMock(return_value=meta_vaults_map),
10051027
) as mock_graph,
1006-
patch(f'{MODULE}.is_meta_vault', new=is_meta_mock),
10071028
patch(
10081029
f'{MODULE}.is_meta_vault_state_update_required',
10091030
new=AsyncMock(return_value=needs_update),
10101031
),
1011-
patch(f'{MODULE}.process_meta_vault_tree', new=process_tree_mock),
1032+
patch(f'{MODULE}.meta_vault_tree_update_state', new=update_state_mock),
10121033
patch(
10131034
f'{MODULE}.get_multiple_harvest_params',
10141035
new=AsyncMock(return_value=harvest_params),
@@ -1018,8 +1039,7 @@ def vault_factory(addr: ChecksumAddress) -> MagicMock:
10181039
):
10191040
yield {
10201041
'graph_get_vaults': mock_graph,
1021-
'is_meta_vault': is_meta_mock,
1022-
'process_tree': process_tree_mock,
1042+
'update_state': update_state_mock,
10231043
'harvest_params': mock_harvest_params,
10241044
'multicall': multicall_mocks['mock_multicall'],
10251045
'vault_cls': mock_vault_cls,

0 commit comments

Comments
 (0)