Skip to content

Commit

Permalink
Enzyme redeem dust fix (#1052)
Browse files Browse the repository at this point in the history
- `enzyme-polygon-eth-btc-rsi` executor was crashing, because Enzyme was redeeming 1 raw unit of aPolUSDC on a dusty closed position
- Have a dust filter on any incoming Enzyme redemption event data
  • Loading branch information
miohtama authored Oct 1, 2024
1 parent f7afb9a commit 96bb703
Show file tree
Hide file tree
Showing 7 changed files with 927 additions and 5 deletions.
1 change: 0 additions & 1 deletion strategies/eth-btc-usdc-size-risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,6 @@ def decide_trades(
alpha_model.normalise_weights(
investable_equity=portfolio_target_value,
size_risk_model=size_risk_model,

)

# Load in old weight for each trading pair signal,
Expand Down
722 changes: 722 additions & 0 deletions strategies/test_only/enzyme-polygon-eth-btc-rsi.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions tests/mainnet_fork/redeem-dust.json

Large diffs are not rendered by default.

150 changes: 150 additions & 0 deletions tests/mainnet_fork/test_enzyme_redeem_dust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""Test Enzyme redemption where the redemption request has a closed position with dust."""
import os
import secrets
from pathlib import Path

import pytest
from _pytest.fixtures import FixtureRequest

from eth_defi.provider.anvil import AnvilLaunch, launch_anvil

from tradeexecutor.cli.main import app


pytestmark = pytest.mark.skipif(not os.environ.get("JSON_RPC_POLYGON") or not os.environ.get("TRADING_STRATEGY_API_KEY"), reason="Set JSON_RPC_POLYGON and TRADING_STRATEGY_API_KEY environment variables to run this test")


@pytest.fixture(scope="module")
def end_block() -> int:
"""The chain point of time when we simulate the events."""
block_time = 2
days = 6
return 62514843 - days*24*3600//block_time


@pytest.fixture()
def anvil(request: FixtureRequest, end_block) -> AnvilLaunch:
mainnet_rpc = os.environ["JSON_RPC_POLYGON"]
anvil = launch_anvil(
mainnet_rpc,
fork_block_number=end_block, # The timestamp on when the broken position was created
)
try:
yield anvil
finally:
#anvil.close(log_level=logging.INFO)
anvil.close()


@pytest.fixture()
def state_file() -> Path:
p = Path(os.path.join(os.path.dirname(__file__), "redeem-dust.json"))
assert p.exists(), f"{p} missing"
return p


@pytest.fixture()
def strategy_file() -> Path:
"""The strategy module where the broken accounting happened."""
p = Path(os.path.join(os.path.dirname(__file__), "..", "..", "strategies", "test_only", "enzyme-polygon-eth-btc-rsi.py"))
assert p.exists(), f"{p.resolve()} missing"
return p


@pytest.fixture()
def environment(
anvil: AnvilLaunch,
state_file: Path,
strategy_file: Path,
end_block: int,
) -> dict:
"""Used by CLI commands, for setting up this test environment"""
environment = {
"STRATEGY_FILE": strategy_file.as_posix(),
"PRIVATE_KEY": "0x" + secrets.token_bytes(32).hex(),
"JSON_RPC_ANVIL": anvil.json_rpc_url,
"STATE_FILE": state_file.as_posix(),
"ASSET_MANAGEMENT_MODE": "enzyme",
"UNIT_TESTING": "true",
"UNIT_TEST_FORCE_ANVIL": "true", # check-wallet command legacy hack
"LOG_LEVEL": "disabled",
# "LOG_LEVEL": "info",
"TRADING_STRATEGY_API_KEY": os.environ["TRADING_STRATEGY_API_KEY"],
"VAULT_ADDRESS": "0xbba6B781e0BAC1798e4E715ef9b1113Bf2387544",
"VAULT_ADAPTER_ADDRESS": "0x519f26bE61889656e83262ab56D75f00DFDAAEc1",
"VAULT_PAYMENT_FORWARDER_ADDRESS": "0x638241c16aB5002298B24ED2F0074B4662042258",
"VAULT_DEPLOYMENT_BLOCK_NUMBER": "60554042",
"SKIP_SAVE": "true",
"PROCESS_REDEMPTION": "true", # Especially test for the broken redemption event
"PROCESS_REDEMPTION_END_BLOCK_HINT": str(end_block),
"AUTO_APPROVE": "true", # Disable interactivity for repair command
}
return environment


@pytest.mark.slow_test_group
def test_enzyme_redeem_dust(
environment: dict,
mocker,
):
"""Test Enzyme redemption where the redemption request has a closed position with dust.
- The state file contains a closed aPolUSDC position
- This position has small dust value left
- Enzyme redemption request tries to redeem part of this dust (1 unit of aPolUSDC)
- Make sure we can handle the dust redemption request on a closed position
The problematic SharesRedeemed event:
.. code-block:: text
enzyme-polygon-eth-btc-rsi | tradeexecutor.ethereum.enzyme.vault.UnknownAsset: Asset <aPolUSDC at 0x625e7708f30ca75bfd92586e17077590c60eb4cd> does not map to any open position.
enzyme-polygon-eth-btc-rsi | Do not know how to recover. You need to stop trade-executor and run accounting correction.
enzyme-polygon-eth-btc-rsi | Could not process redemption event.
enzyme-polygon-eth-btc-rsi | Redeemed assets:
enzyme-polygon-eth-btc-rsi | <USD Coin (PoS) (USDC) at 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174, 6 decimals, on chain 137>: 84614952
enzyme-polygon-eth-btc-rsi | <Wrapped Ether (WETH) at 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619, 18 decimals, on chain 137>: 0
enzyme-polygon-eth-btc-rsi | <(PoS) Wrapped BTC (WBTC) at 0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6, 8 decimals, on chain 137>: 0
enzyme-polygon-eth-btc-rsi | <Aave Polygon USDC (aPolUSDC) at 0x625E7708f30cA75bfd92586e17077590C60eb4cD, 6 decimals, on chain 137>: 1
enzyme-polygon-eth-btc-rsi | EVM event data:
enzyme-polygon-eth-btc-rsi | { 'address': '0xd4d6e8a69a6d4bebbb96188cfc6465d91ae461d6',
enzyme-polygon-eth-btc-rsi | 'blockHash': '0xfa29b55146628d84f3b2990a2458e18064e2daa4427112ace270c40fd879f9d8',
enzyme-polygon-eth-btc-rsi | 'blockNumber': 62094345,
enzyme-polygon-eth-btc-rsi | 'chunk_id': 62093612,
enzyme-polygon-eth-btc-rsi | 'context': None,
enzyme-polygon-eth-btc-rsi | 'data': '0x000000000000000000000000000000000000000000000004e3f298eb47ba54fb0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000040000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000001bfd67037b42cf73acf2047067bd4f2c47d9bfd6000000000000000000000000625e7708f30ca75bfd92586e17077590c60eb4cd000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000050b1f28000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',
enzyme-polygon-eth-btc-rsi | 'event': <class 'web3._utils.datatypes.SharesRedeemed'>,
enzyme-polygon-eth-btc-rsi | 'logIndex': '0x76',
enzyme-polygon-eth-btc-rsi | 'removed': False,
enzyme-polygon-eth-btc-rsi | 'timestamp': 1726917217,
enzyme-polygon-eth-btc-rsi | 'topics': [ HexBytes('0xbf88879a1555e4d7d38ebeffabce61fdf5e12ea0468abf855a72ec17b432bed5'),
enzyme-polygon-eth-btc-rsi | HexBytes('0x000000000000000000000000c37b40abdb939635068d3c5f13e7faf686f03b65'),
enzyme-polygon-eth-btc-rsi | HexBytes('0x000000000000000000000000c37b40abdb939635068d3c5f13e7faf686f03b65')],
enzyme-polygon-eth-btc-rsi | 'transactionHash': '0x0ac5a9eb7f426d3da655549c539bbad6919a9f6addbae3527d33725859e2e02c',
enzyme-polygon-eth-btc-rsi | 'transactionIndex': '0x1f'}
When running with log level info, the test should spit out warning:
.. code-block:: text
2024-10-01 23:03:34 tradeexecutor.ethereum.enzyme.vault WARNING Enzyme dust redemption detected and ignored.
Asset: <aPolUSDC at 0x625e7708f30ca75bfd92586e17077590c60eb4cd>
Epsilon: 0.1000000000000000055511151231257827021181583404541015625
Amount: 0.000001
"""

mocker.patch.dict("os.environ", environment, clear=True)

app(["repair"], standalone_mode=False)

with pytest.raises(SystemExit) as sys_exit:
app(["correct-accounts"], standalone_mode=False)
assert sys_exit.value.code == 0


28 changes: 28 additions & 0 deletions tradeexecutor/cli/commands/correct_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ def correct_accounts(
chain_settle_wait_seconds: float = Option(60.0, "--chain-settle-wait-seconds", envvar="CHAIN_SETTLE_WAIT_SECONDS", help="How long we wait after the account correction to see if our broadcasted transactions fixed the issue."),
skip_save: bool = Option(False, "--skip-save", envvar="SKIP_SAVE", help="Do not update state file. Useful for testing."),
skip_interest: bool = Option(False, "--skip-interest", envvar="SKIP_INTEREST", help="Do not do interest distribution. If an position balance is fixed down due to redemption, this is useful."),
process_redemption: bool = Option(False, "--process-redemption", envvar="PROCESS_REDEMPTION", help="Attempt to process deposit and redemption requests before correcting accounts."),
process_redemption_end_block_hint: int = Option(None, "--process-redemption-end-block-hint", envvar="PROCESS_REDEMPTION_END_BLOCK_HINT", help="Used in integration testing."),
transfer_away: bool = Option(False, "--transfer-away", envvar="TRANSFER_AWAY", help="For tokens without assigned position, scoop them to the hot wallet instead of trying to construct a new position"),

):
Expand Down Expand Up @@ -303,6 +305,32 @@ def correct_accounts(
else:
logger.info("Interest distribution skipped")

if process_redemption:
timestamp = datetime.datetime.utcnow()
reserve_assets = list(universe.reserve_assets)


if process_redemption_end_block_hint:
# Passed by unit tests so we are not going to scan the whole chain until today (wall clock time)
end_block = process_redemption_end_block_hint
else:
end_block = execution_model.get_safe_latest_block()

logger.info(
"Processing deposits/redemptions, timestamp set to %s, reserves are %s, end block is %d",
timestamp,
reserve_assets,
end_block,
)

sync_model.sync_treasury(
strategy_cycle_ts=timestamp,
state=state,
end_block=end_block,
)
else:
logger.info("Deposit/redemption distribution skipped")

balance_updates = _correct_accounts(
state,
corrections,
Expand Down
19 changes: 19 additions & 0 deletions tradeexecutor/ethereum/enzyme/vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from tradeexecutor.state.balance_update import BalanceUpdate, BalanceUpdateCause, BalanceUpdatePositionType
from tradeexecutor.state.sync import BalanceEventRef
from tradeexecutor.state.types import BlockNumber
from tradeexecutor.strategy.dust import get_dust_epsilon_for_asset
from tradeexecutor.strategy.sync_model import SyncModel, OnChainBalance
from tradingstrategy.chain import ChainId
from tradeexecutor.strategy.trading_strategy_universe import TradingStrategyUniverse
Expand Down Expand Up @@ -262,6 +263,24 @@ def process_redemption(self, portfolio: Portfolio, event: Redemption, strategy_c

asset = translate_token_details(token_details)

amount = asset.convert_to_decimal(raw_amount)

# Check for dusty redemption.
# We may have already closed the position,
# but on Enzyme's books it is still open with dust left.
# In this case, any redemption request may attempt to redeem this dust.
# See test_enzyme_redeem_dust.
dust_epsilon = get_dust_epsilon_for_asset(asset)
if amount < dust_epsilon:
# Use warning so for now we will flag these issues better
logger.warning(
f"Enzyme dust redemption detected and ignored.\n"
f"Asset: {asset}\n"
f"Epsilon: {dust_epsilon}\n"
f"Amount: {amount}\n"
)
continue

try:
position = self.get_related_position(portfolio, asset)
except UnknownAsset as e:
Expand Down
11 changes: 7 additions & 4 deletions tradeexecutor/strategy/account_correction.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,10 @@ def is_mismatch(self) -> bool:


def is_relative_mismatch(
actual_amount,
expected_amount,
relative_epsilon,
dust_epsilon,
actual_amount: Decimal,
expected_amount: Decimal,
relative_epsilon: float | Decimal,
dust_epsilon: float | Decimal,
) -> bool:
"""Calculate if we are within the relative tolerance.
Expand All @@ -209,6 +209,9 @@ def is_relative_mismatch(
- Relative % of the position size
"""

assert isinstance(actual_amount, Decimal), f"Got {type(actual_amount)}"
assert isinstance(expected_amount, Decimal), f"Got {type(expected_amount)}"

# Accounting dust.
# The position has been closed but we have left fractions of tokens on the account.
# Cannot be compared with relative match.
Expand Down

0 comments on commit 96bb703

Please sign in to comment.