Skip to content

Commit f35f76b

Browse files
Reserve redemption assets
1 parent 22c583b commit f35f76b

6 files changed

Lines changed: 94 additions & 59 deletions

File tree

src/commands/start/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ async def process_block(self, interrupt_handler: InterruptHandler) -> None:
130130
chain_head=chain_head, interrupt_handler=interrupt_handler
131131
)
132132
await scan_validators_events(block_number=chain_head.block_number, is_startup=False)
133-
subtasks = [self.validator_registration_subtask.process()]
133+
subtasks = [self.validator_registration_subtask.process(chain_head=chain_head)]
134134
if not settings.disable_withdrawals:
135135
subtasks.append(self.validator_withdrawal_subtask.process())
136136
await asyncio.gather(*subtasks)

src/meta_vault/service.py

Lines changed: 35 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections import defaultdict
2-
from typing import cast
2+
from queue import Queue
33

44
from eth_typing import BlockNumber, ChecksumAddress
55
from sw_utils import is_meta_vault_upgraded_to_release, memoize
@@ -13,73 +13,67 @@
1313
VaultContract,
1414
)
1515
from src.config.settings import settings
16+
from src.meta_vault.typings import SubVaultRedemption
1617

1718

1819
async def distribute_meta_vault_redemption_assets(
19-
vault_to_redemption_assets: defaultdict[ChecksumAddress, Wei],
20+
vault_to_redemption_assets: dict[ChecksumAddress, Wei],
2021
block_number: BlockNumber | None = None,
2122
) -> defaultdict[ChecksumAddress, Wei]:
2223
"""
2324
Parameters:
2425
vault_to_redemption_assets: A mapping of vault addresses to their respective redemption assets,
2526
which may include meta vaults.
2627
27-
Distribute redemption assets from meta vaults to their underlying sub-vaults.
28+
Distribute redemption assets from meta vaults across their entire sub-vault tree.
2829
Returns a mapping of vault addresses to their respective redemption assets,
29-
ensuring all assets are assigned to non-meta vaults.
30+
including leaf vaults, root meta vaults, and all intermediary meta sub-vaults.
3031
"""
31-
final_vault_to_redemption_assets: defaultdict[ChecksumAddress, int] = defaultdict(lambda: 0)
32+
final_vault_to_redemption_assets: defaultdict[ChecksumAddress, Wei] = defaultdict(
33+
lambda: Wei(0)
34+
)
3235

36+
queue: Queue[tuple[ChecksumAddress, Wei]] = Queue()
3337
for vault, assets in vault_to_redemption_assets.items():
34-
if await is_meta_vault(vault):
35-
sub_vaults_redemptions = await get_meta_vault_redemption_assets(
36-
meta_vault_address=vault,
37-
assets_to_redeem=assets,
38-
block_number=block_number,
39-
)
40-
for sub_vault, sub_assets in sub_vaults_redemptions.items():
41-
final_vault_to_redemption_assets[sub_vault] += sub_assets
42-
else:
43-
final_vault_to_redemption_assets[vault] += assets
44-
45-
return cast(defaultdict[ChecksumAddress, Wei], final_vault_to_redemption_assets)
46-
47-
48-
async def get_meta_vault_redemption_assets(
38+
queue.put((vault, assets))
39+
40+
while not queue.empty():
41+
vault, assets = queue.get()
42+
final_vault_to_redemption_assets[vault] = Wei(
43+
final_vault_to_redemption_assets[vault] + assets
44+
)
45+
if not await is_meta_vault(vault):
46+
continue
47+
sub_vaults_redemptions = await get_sub_vaults_redemptions(
48+
meta_vault_address=vault,
49+
assets_to_redeem=assets,
50+
block_number=block_number,
51+
)
52+
for sub_vault_redemption in sub_vaults_redemptions:
53+
queue.put((sub_vault_redemption.vault, sub_vault_redemption.assets))
54+
55+
return final_vault_to_redemption_assets
56+
57+
58+
async def get_sub_vaults_redemptions(
4959
meta_vault_address: ChecksumAddress,
5060
assets_to_redeem: Wei,
5161
block_number: BlockNumber | None = None,
52-
) -> defaultdict[ChecksumAddress, Wei]:
62+
) -> list[SubVaultRedemption]:
5363
"""
54-
This function distributes the specified assets to redeem from the meta vault
55-
among its underlying sub-vaults. It handles both regular and nested meta vaults.
56-
Finally every asset should be assigned to a non-meta vault.
64+
Distribute the specified assets to redeem from the meta vault across its
65+
direct sub-vaults only. Does not recurse into nested meta vaults.
5766
58-
Returns a mapping of vault addresses to their respective redemption assets.
67+
Returns a list of SubVaultRedemption entries, one per direct sub-vault.
5968
"""
60-
vault_to_redemption_assets: defaultdict[ChecksumAddress, int] = defaultdict(lambda: 0)
6169
meta_vault_contract = MetaVaultContract(meta_vault_address)
62-
6370
sub_vaults_registry_address = await meta_vault_contract.sub_vaults_registry()
6471
sub_vaults_registry_contract = SubVaultsRegistryContract(sub_vaults_registry_address)
6572

6673
sub_vaults_redemptions = await sub_vaults_registry_contract.calculate_sub_vaults_redemptions(
6774
assets_to_redeem, block_number=block_number
6875
)
69-
70-
for sub_vault_redemption in sub_vaults_redemptions:
71-
if sub_vault_redemption.assets > 0 and await is_meta_vault(sub_vault_redemption.vault):
72-
sub_vault_assets = await get_meta_vault_redemption_assets(
73-
meta_vault_address=sub_vault_redemption.vault,
74-
assets_to_redeem=sub_vault_redemption.assets,
75-
block_number=block_number,
76-
)
77-
for vault, assets in sub_vault_assets.items():
78-
vault_to_redemption_assets[vault] += assets
79-
else:
80-
vault_to_redemption_assets[sub_vault_redemption.vault] += sub_vault_redemption.assets
81-
82-
return cast(defaultdict[ChecksumAddress, Wei], vault_to_redemption_assets)
76+
return sub_vaults_redemptions
8377

8478

8579
@memoize

src/meta_vault/tasks.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import logging
2+
from collections import defaultdict
23

34
from eth_typing import ChecksumAddress, HexStr
45
from hexbytes import HexBytes
56
from sw_utils import GNO_NETWORKS, InterruptHandler, convert_to_mgno
67
from web3 import Web3
7-
from web3.types import BlockNumber
8+
from web3.types import BlockNumber, Wei
89

910
from src.common.clients import execution_client
11+
from src.common.consensus import get_chain_latest_head
1012
from src.common.contracts import (
1113
MetaVaultContract,
1214
MetaVaultEncoder,
@@ -28,6 +30,7 @@
2830
graph_get_vaults,
2931
)
3032
from src.meta_vault.typings import ContractCall, SubVaultExitRequest, Vault
33+
from src.redemptions.tasks import get_redemption_reservations
3134

3235
logger = logging.getLogger(__name__)
3336

@@ -50,6 +53,9 @@ async def process_block(self, interrupt_handler: InterruptHandler) -> None:
5053
if not await check_gas_price():
5154
return
5255

56+
chain_head = await get_chain_latest_head()
57+
meta_vault_reservations = await get_redemption_reservations(chain_head)
58+
5359
for vault in self.vaults:
5460
# Refresh the meta vaults map to include updates
5561
# made by previous meta vault transactions
@@ -58,14 +64,19 @@ async def process_block(self, interrupt_handler: InterruptHandler) -> None:
5864
is_meta_vault=True,
5965
)
6066
try:
61-
await process_meta_vault_tree(vault=vault, meta_vaults_map=meta_vaults_map)
67+
await process_meta_vault_tree(
68+
vault=vault,
69+
meta_vaults_map=meta_vaults_map,
70+
meta_vault_reservations=meta_vault_reservations,
71+
)
6272
except Exception:
6373
logger.exception('Failed to process meta vault tree for vault %s', vault)
6474

6575

6676
async def process_meta_vault_tree(
6777
vault: ChecksumAddress,
6878
meta_vaults_map: dict[ChecksumAddress, Vault],
79+
meta_vault_reservations: defaultdict[ChecksumAddress, Wei],
6980
) -> None:
7081
"""
7182
Process a single meta vault tree: update state and deposit to sub vaults.
@@ -103,7 +114,10 @@ async def process_meta_vault_tree(
103114
)
104115
# Reverse to get top-down order: root deposits first, then nested meta vaults
105116
for meta_vault_address in reversed(meta_vault_addresses):
106-
await process_deposit_to_sub_vaults(meta_vault_address=meta_vault_address)
117+
await process_deposit_to_sub_vaults(
118+
meta_vault_address=meta_vault_address,
119+
redemption_reservation=meta_vault_reservations[meta_vault_address],
120+
)
107121

108122

109123
async def meta_vault_tree_update_state(
@@ -388,12 +402,18 @@ async def is_meta_vault_rewards_nonce_outdated(
388402
return meta_vault_event is None
389403

390404

391-
async def process_deposit_to_sub_vaults(meta_vault_address: ChecksumAddress) -> None:
405+
async def process_deposit_to_sub_vaults(
406+
meta_vault_address: ChecksumAddress,
407+
redemption_reservation: Wei = Wei(0),
408+
) -> None:
392409
meta_vault_contract = MetaVaultContract(
393410
address=meta_vault_address,
394411
)
395412
withdrawable_assets = await meta_vault_contract.withdrawable_assets()
396413

414+
# Reserve redemption assets so they are not pushed down to sub vaults.
415+
withdrawable_assets = Wei(max(0, withdrawable_assets - redemption_reservation))
416+
397417
if settings.network in GNO_NETWORKS:
398418
withdrawable_assets = convert_to_mgno(withdrawable_assets)
399419

src/meta_vault/tests/test_service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def is_meta_vault_side_effect(addr):
7070
result = await distribute_meta_vault_redemption_assets(assets)
7171
assert dict(result) == {
7272
vaults['vault1']: 100,
73+
vaults['meta_vault']: 300,
7374
vaults['sub_vault1']: 120,
7475
vaults['sub_vault2']: 180,
7576
}
@@ -121,6 +122,8 @@ async def sub_vaults_registry_side_effect(self):
121122
):
122123
result = await distribute_meta_vault_redemption_assets(assets)
123124
assert dict(result) == {
125+
vaults['meta_vault']: 500,
126+
vaults['nested_meta_vault']: 300,
124127
vaults['vault2']: 200,
125128
vaults['sub_vault1']: 100,
126129
vaults['sub_vault2']: 200,
@@ -159,6 +162,7 @@ def is_meta_vault_side_effect(addr):
159162
result = await distribute_meta_vault_redemption_assets(assets)
160163
assert dict(result) == {
161164
vaults['vault1']: 50,
165+
vaults['meta_vault']: 150,
162166
vaults['vault2']: 75,
163167
vaults['sub_vault1']: 60,
164168
vaults['sub_vault2']: 90,

src/redemptions/tasks.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,35 @@ async def get_redemption_assets(chain_head: ChainHead) -> Wei:
2828
Get redemption assets for operator's vault.
2929
For Gno networks return value in GNO-Wei.
3030
"""
31+
post_distribute = await get_redemption_reservations(chain_head)
32+
return post_distribute[settings.vault]
33+
34+
35+
async def get_redemption_reservations(
36+
chain_head: ChainHead,
37+
) -> defaultdict[ChecksumAddress, Wei]:
38+
"""
39+
Get per-vault redemption reservations after distributing meta vault assets
40+
to sub-vaults. Contains only non-meta vaults.
41+
42+
For Gno networks values are in GNO-Wei.
43+
"""
3144
# The contract increments nonce during setRedeemablePositions,
3245
# but uses nonce - 1 for leaf hash computation during redemption.
3346
nonce = await os_token_redeemer_contract.nonce(chain_head.block_number)
3447
if nonce == 0:
3548
logger.info('Zero nonce for redemption. Skipping redemption assets.')
36-
return Wei(0)
49+
return defaultdict(lambda: Wei(0))
3750

3851
protocol_config = await get_protocol_config()
3952

40-
# Aggregate redemption assets per vault
41-
vault_to_redemption_assets = await get_vault_to_redemption_assets(
53+
pre_distribute = await get_vault_to_redemption_assets(
4254
chain_head=chain_head, tree_nonce=nonce - 1, protocol_config=protocol_config
4355
)
44-
# Distribute redemption assets from meta vaults to their underlying vaults
45-
vault_to_redemption_assets = await distribute_meta_vault_redemption_assets(
46-
vault_to_redemption_assets=vault_to_redemption_assets,
56+
return await distribute_meta_vault_redemption_assets(
57+
vault_to_redemption_assets=pre_distribute,
4758
block_number=chain_head.block_number,
4859
)
49-
# Filter by operator's vault
50-
return vault_to_redemption_assets[settings.vault]
5160

5261

5362
async def get_vault_to_redemption_assets(

src/validators/tasks.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from typing import Sequence, cast
44

55
from eth_typing import HexStr
6-
from sw_utils import IpfsFetchClient, convert_to_mgno
6+
from sw_utils import ChainHead, IpfsFetchClient, convert_to_mgno
77
from sw_utils.networks import GNO_NETWORKS
88
from web3 import Web3
9-
from web3.types import BlockNumber, Gwei
9+
from web3.types import BlockNumber, Gwei, Wei
1010

1111
from src.common.clients import execution_client
1212
from src.common.contracts import VaultContract, validators_registry_contract
@@ -20,6 +20,7 @@
2020
VALIDATORS_FUNDING_BATCH_SIZE,
2121
settings,
2222
)
23+
from src.redemptions.tasks import get_redemption_reservations
2324
from src.validators.consensus import fetch_compounding_validators_balances
2425
from src.validators.database import NetworkValidatorCrud
2526
from src.validators.exceptions import EmptyRelayerResponseException, FundingException
@@ -45,14 +46,14 @@ def __init__(
4546
self.keystore = keystore
4647
self.relayer = relayer
4748

48-
async def process(self) -> None:
49+
async def process(self, chain_head: ChainHead) -> None:
4950
if self.keystore:
5051
await update_unused_validator_keys_metric(
5152
keystore=self.keystore,
5253
)
5354
harvest_params = await get_harvest_params(settings.vault)
5455

55-
vault_assets = await get_vault_assets(harvest_params=harvest_params)
56+
vault_assets = await get_vault_assets(harvest_params=harvest_params, chain_head=chain_head)
5657
vault_assets = Gwei(max(0, vault_assets - settings.vault_min_balance_gwei))
5758

5859
if vault_assets < settings.min_deposit_amount_gwei:
@@ -261,8 +262,15 @@ async def register_new_validators(
261262
return tx_hash
262263

263264

264-
async def get_vault_assets(harvest_params: HarvestParams | None) -> Gwei:
265+
async def get_vault_assets(harvest_params: HarvestParams | None, chain_head: ChainHead) -> Gwei:
265266
vault_assets = await get_withdrawable_assets(settings.vault, harvest_params=harvest_params)
267+
268+
# Reserve operator's vault redemption assets so that withdrawable assets are spent
269+
# on redemptions first, then on validator registrations / fundings.
270+
post_distribute = await get_redemption_reservations(chain_head)
271+
reservation = post_distribute[settings.vault]
272+
vault_assets = Wei(max(0, vault_assets - reservation))
273+
266274
if settings.network in GNO_NETWORKS:
267275
# apply GNO -> mGNO exchange rate
268276
vault_assets = convert_to_mgno(vault_assets)

0 commit comments

Comments
 (0)