Skip to content

Commit dc3cf41

Browse files
committed
Review fixes
Signed-off-by: cyc60 <avsysoev60@gmail.com>
1 parent 3e10f31 commit dc3cf41

3 files changed

Lines changed: 76 additions & 68 deletions

File tree

src/commands/internal/process_redeemer.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pathlib import Path
66

77
import click
8-
from eth_typing import BlockNumber, ChecksumAddress, HexStr
8+
from eth_typing import BlockNumber, ChecksumAddress
99
from hexbytes import HexBytes
1010
from multiproof import StandardMerkleTree
1111
from multiproof.standard import MultiProof
@@ -18,8 +18,6 @@
1818
from src.common.contracts import (
1919
MetaVaultContract,
2020
SubVaultsRegistryContract,
21-
VaultContract,
22-
multicall_contract,
2321
os_token_redeemer_contract,
2422
)
2523
from src.common.execution import (
@@ -29,6 +27,7 @@
2927
)
3028
from src.common.harvest import get_multiple_harvest_params
3129
from src.common.logging import LOG_LEVELS, setup_logging
30+
from src.common.typings import HarvestParams
3231
from src.common.utils import log_verbose
3332
from src.common.wallet import wallet
3433
from src.config.networks import AVAILABLE_NETWORKS, ZERO_CHECKSUM_ADDRESS
@@ -266,7 +265,7 @@ async def _redeem_os_token_positions(
266265

267266
# Bring every involved vault up to date on-chain so subsequent reads
268267
# and redemption transactions run against fresh state — no harvest_params
269-
# plumbing and no updateVaultState bundled in a multicall.
268+
# plumbing through redeemOsTokenPositions.
270269
vaults = list({position.vault for position in os_token_positions})
271270
await update_vaults_state(vaults=vaults, block_number=block_number)
272271

@@ -328,10 +327,11 @@ async def update_vaults_state(
328327
"""Bring every vault in ``vaults`` up to date on-chain.
329328
330329
Meta vaults that need a state update are harvested via process_meta_vault_tree.
331-
Regular vaults with pending updates are batched into a single multicall
332-
submitting ``updateState`` for each. After this call, ``get_withdrawable_assets``
333-
reflects fresh state, so subsequent redemption decisions can be made without
334-
threading harvest_params through every read.
330+
Regular vaults with pending updates are batched into a single ``multicall`` on
331+
the OsTokenRedeemer contract submitting ``updateVaultState`` for each, so the
332+
transaction is attributed to the redeemer on Etherscan. After this call,
333+
``get_withdrawable_assets`` reflects fresh state, so subsequent redemption
334+
decisions can be made without threading harvest_params through every read.
335335
"""
336336
regular_vaults: list[ChecksumAddress] = []
337337
meta_vaults_map = await graph_get_vaults(
@@ -371,28 +371,28 @@ async def update_vaults_state(
371371
if not regular_vaults:
372372
return
373373

374-
vault_to_harvest_params = await get_multiple_harvest_params(regular_vaults, block_number)
375-
calls: list[tuple[ChecksumAddress, HexStr]] = []
376-
for vault in regular_vaults:
377-
harvest_params = vault_to_harvest_params.get(vault)
378-
if harvest_params is None:
379-
continue
380-
vault_contract = VaultContract(vault)
381-
calls.append(
382-
(vault_contract.contract_address, vault_contract.get_update_state_call(harvest_params))
383-
)
384-
385-
if not calls:
374+
vault_to_harvest_params: dict[ChecksumAddress, HarvestParams] = {
375+
vault: harvest_params
376+
for vault, harvest_params in (
377+
await get_multiple_harvest_params(regular_vaults, block_number)
378+
).items()
379+
if harvest_params is not None
380+
}
381+
if not vault_to_harvest_params:
386382
return
387383

388-
tx_hash = await multicall_contract.tx_aggregate(calls)
389-
logger.info('Waiting for Update State multicall tx %s confirmation', tx_hash)
384+
tx_hash = await os_token_redeemer_contract.update_vaults_state(vault_to_harvest_params)
385+
logger.info(
386+
'Waiting for OsTokenRedeemer updateVaultState multicall tx %s confirmation', tx_hash
387+
)
390388
tx_receipt = await execution_client.eth.wait_for_transaction_receipt(
391389
HexBytes(Web3.to_bytes(hexstr=tx_hash)), timeout=settings.execution_transaction_timeout
392390
)
393391
if not tx_receipt['status']:
394-
raise RuntimeError(f'Update State multicall tx failed. Tx Hash: {tx_hash}')
395-
logger.info('Update State multicall confirmed. Tx Hash: %s', tx_hash)
392+
raise RuntimeError(
393+
f'OsTokenRedeemer updateVaultState multicall tx failed. Tx Hash: {tx_hash}'
394+
)
395+
logger.info('OsTokenRedeemer updateVaultState multicall confirmed. Tx Hash: %s', tx_hash)
396396

397397

398398
async def redeem_positions(

src/commands/tests/test_internal/test_process_redeemer.py

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from unittest.mock import AsyncMock, MagicMock, patch
55

66
import pytest
7-
from eth_typing import BlockNumber, ChecksumAddress, HexStr
7+
from eth_typing import BlockNumber, ChecksumAddress
88
from hexbytes import HexBytes
99
from sw_utils.tests import faker
1010
from web3 import Web3
@@ -518,16 +518,15 @@ async def test_meta_vault_claim_delay_logged_and_continues(self) -> None:
518518
exit_request = MagicMock()
519519
exit_request.vault = VAULT_1
520520
exit_request.position_ticket = 1234
521+
params = make_harvest_params()
521522
with _mock_update_vaults_state(
522523
meta_vaults_map={VAULT_1: MagicMock()},
523-
harvest_params={VAULT_2: make_harvest_params()},
524+
harvest_params={VAULT_2: params},
524525
update_state_exception=ClaimDelayNotPassedException(exit_request),
525526
) as mocks:
526527
await update_vaults_state(vaults=[VAULT_1, VAULT_2], block_number=BlockNumber(100))
527528
mocks['update_state'].assert_awaited_once()
528-
mocks['multicall'].tx_aggregate.assert_awaited_once_with(
529-
[(VAULT_2, ENCODED_UPDATE_STATE_CALL)]
530-
)
529+
mocks['redeemer'].update_vaults_state.assert_awaited_once_with({VAULT_2: params})
531530

532531
@pytest.mark.parametrize(
533532
'has_params, expected_multicall_calls',
@@ -542,25 +541,25 @@ async def test_regular_vault_multicall_gated_by_harvest_params(
542541
with _mock_update_vaults_state(harvest_params={VAULT_1: params}) as mocks:
543542
await update_vaults_state(vaults=[VAULT_1], block_number=BlockNumber(100))
544543

545-
assert mocks['multicall'].tx_aggregate.await_count == expected_multicall_calls
544+
assert mocks['redeemer'].update_vaults_state.await_count == expected_multicall_calls
546545
if has_params:
547-
mocks['multicall'].tx_aggregate.assert_awaited_once_with(
548-
[(VAULT_1, ENCODED_UPDATE_STATE_CALL)]
549-
)
546+
mocks['redeemer'].update_vaults_state.assert_awaited_once_with({VAULT_1: params})
550547

551548
async def test_multicall_tx_failure_raises(self) -> None:
552549
"""A failed multicall receipt aborts the round."""
553550
with _mock_update_vaults_state(
554551
harvest_params={VAULT_1: make_harvest_params()},
555552
multicall_tx_status=0,
556553
):
557-
with pytest.raises(RuntimeError, match='Update State multicall tx failed'):
554+
with pytest.raises(
555+
RuntimeError, match='OsTokenRedeemer updateVaultState multicall tx failed'
556+
):
558557
await update_vaults_state(vaults=[VAULT_1], block_number=BlockNumber(100))
559558

560559
async def test_mix_of_meta_and_regular_vaults(self) -> None:
561560
"""Meta vault is harvested via meta_vault_tree_update_state; regular vaults are
562-
batched into a single multicall. Harvest params are fetched only for the
563-
regular vaults."""
561+
batched into a single multicall on the OsTokenRedeemer. Harvest params are
562+
fetched only for the regular vaults."""
564563
params = make_harvest_params()
565564
meta_vaults_map = {VAULT_1: MagicMock()}
566565
with _mock_update_vaults_state(
@@ -574,9 +573,7 @@ async def test_mix_of_meta_and_regular_vaults(self) -> None:
574573
mocks['update_state'].await_args.kwargs['root_meta_vault'] is meta_vaults_map[VAULT_1]
575574
)
576575
mocks['harvest_params'].assert_awaited_once_with([VAULT_2], BlockNumber(100))
577-
mocks['multicall'].tx_aggregate.assert_awaited_once_with(
578-
[(VAULT_2, ENCODED_UPDATE_STATE_CALL)]
579-
)
576+
mocks['redeemer'].update_vaults_state.assert_awaited_once_with({VAULT_2: params})
580577

581578

582579
class TestRedeemMetaVaultSubVaults:
@@ -987,9 +984,6 @@ def _mock_submit_redeem_position(
987984
}
988985

989986

990-
ENCODED_UPDATE_STATE_CALL = HexStr('0xdeadbeef')
991-
992-
993987
@contextmanager
994988
def _mock_update_vaults_state(
995989
needs_update: bool = True,
@@ -1004,7 +998,8 @@ def _mock_update_vaults_state(
1004998
in this map are treated as meta vaults by update_vaults_state. ``harvest_params``
1005999
is the dict returned by get_multiple_harvest_params; a None value for a vault
10061000
skips it from the multicall (production behavior). ``update_state_exception``
1007-
makes meta_vault_tree_update_state raise.
1001+
makes meta_vault_tree_update_state raise. ``multicall_tx_status`` controls the
1002+
receipt status of the OsTokenRedeemer.update_vaults_state batched tx.
10081003
"""
10091004
meta_vaults_map = {} if meta_vaults_map is None else meta_vaults_map
10101005
harvest_params = {} if harvest_params is None else harvest_params
@@ -1014,11 +1009,10 @@ def _mock_update_vaults_state(
10141009
else:
10151010
update_state_mock = AsyncMock()
10161011

1017-
def vault_factory(addr: ChecksumAddress) -> MagicMock:
1018-
mock_vault = MagicMock()
1019-
mock_vault.contract_address = addr
1020-
mock_vault.get_update_state_call = MagicMock(return_value=ENCODED_UPDATE_STATE_CALL)
1021-
return mock_vault
1012+
mock_client = AsyncMock()
1013+
mock_client.eth.wait_for_transaction_receipt = AsyncMock(
1014+
return_value={'status': multicall_tx_status}
1015+
)
10221016

10231017
with (
10241018
patch(
@@ -1034,34 +1028,20 @@ def vault_factory(addr: ChecksumAddress) -> MagicMock:
10341028
f'{MODULE}.get_multiple_harvest_params',
10351029
new=AsyncMock(return_value=harvest_params),
10361030
) as mock_harvest_params,
1037-
patch(f'{MODULE}.VaultContract', side_effect=vault_factory) as mock_vault_cls,
1038-
_mock_multicall_tx(tx_status=multicall_tx_status) as multicall_mocks,
1031+
patch(f'{MODULE}.os_token_redeemer_contract') as mock_redeemer,
1032+
patch(f'{MODULE}.execution_client', new=mock_client),
1033+
patch(f'{MODULE}.settings') as mock_settings,
10391034
):
1035+
mock_settings.execution_transaction_timeout = 120
1036+
mock_redeemer.update_vaults_state = AsyncMock(return_value='0x' + '11' * 32)
10401037
yield {
10411038
'graph_get_vaults': mock_graph,
10421039
'update_state': update_state_mock,
10431040
'harvest_params': mock_harvest_params,
1044-
'multicall': multicall_mocks['mock_multicall'],
1045-
'vault_cls': mock_vault_cls,
1041+
'redeemer': mock_redeemer,
10461042
}
10471043

10481044

1049-
@contextmanager
1050-
def _mock_multicall_tx(tx_status: int = 1) -> Iterator[dict[str, MagicMock]]:
1051-
"""Mock multicall_contract.tx_aggregate with a fake tx hash + receipt status."""
1052-
mock_client = AsyncMock()
1053-
mock_client.eth.wait_for_transaction_receipt = AsyncMock(return_value={'status': tx_status})
1054-
1055-
with (
1056-
patch(f'{MODULE}.multicall_contract') as mock_multicall,
1057-
patch(f'{MODULE}.execution_client', new=mock_client),
1058-
patch(f'{MODULE}.settings') as mock_settings,
1059-
):
1060-
mock_settings.execution_transaction_timeout = 120
1061-
mock_multicall.tx_aggregate = AsyncMock(return_value='0x' + '11' * 32)
1062-
yield {'mock_multicall': mock_multicall}
1063-
1064-
10651045
@contextmanager
10661046
def _mock_process(
10671047
positions: list[OsTokenPosition] | None = None,

src/common/contracts.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,34 @@ async def redeem_sub_vaults_assets(
619619
)
620620
return Web3.to_hex(tx_hash), tx_receipt['blockNumber']
621621

622+
async def update_vaults_state(
623+
self, vault_to_harvest_params: dict[ChecksumAddress, HarvestParams]
624+
) -> HexStr:
625+
"""Batch ``updateVaultState`` calls into a single multicall on this contract."""
626+
calls = [
627+
self._encode_update_vault_state(vault, harvest_params)
628+
for vault, harvest_params in vault_to_harvest_params.items()
629+
]
630+
tx_function = self.contract.functions.multicall(calls)
631+
tx_hash = await transaction_gas_wrapper(tx_function)
632+
return Web3.to_hex(tx_hash)
633+
634+
def _encode_update_vault_state(
635+
self, vault: ChecksumAddress, harvest_params: HarvestParams
636+
) -> HexStr:
637+
return self.encode_abi(
638+
'updateVaultState',
639+
[
640+
vault,
641+
(
642+
harvest_params.rewards_root,
643+
harvest_params.reward,
644+
harvest_params.unlocked_mev_reward,
645+
harvest_params.proof,
646+
),
647+
],
648+
)
649+
622650

623651
class ValidatorsCheckerContract(ContractWrapper):
624652
abi_path = 'abi/IValidatorsChecker.json'

0 commit comments

Comments
 (0)