From a75a0919076797b600f3a950bdcb3f6af5af351f Mon Sep 17 00:00:00 2001 From: Dylan Paiton Date: Tue, 12 Nov 2024 08:39:38 +0700 Subject: [PATCH] fix invariant check to not require stateful variables (#1731) The negative interest check now directly look up previous pool states instead of updating a stateful variable. other fixes: - update hyperdrivetypes - rename `interface.get_deploy_block()` to `interface.get_deploy_block_number()` and change output type from `int | None` to `BlockNumber | None` - fix import bugs caused by latest pypechain update - rename `block_before_timestamp.py` to match function, `block_number_before_timestamp` - fix utils init file so that only functions are listed under imports --- pyproject.toml | 3 +- .../interactive/local_hyperdrive.py | 2 +- src/agent0/ethpy/base/transactions.py | 3 +- .../hyperdrive/interface/read_interface.py | 17 ++-- .../fork_fuzz/accrue_interest_ezeth.py | 2 +- .../hyperfuzz/system_fuzz/invariant_checks.py | 88 +++++++------------ src/agent0/utils/__init__.py | 7 +- ...mp.py => block_number_before_timestamp.py} | 0 ... => block_number_before_timestamp_test.py} | 2 +- 9 files changed, 55 insertions(+), 69 deletions(-) rename src/agent0/utils/{block_before_timestamp.py => block_number_before_timestamp.py} (100%) rename src/agent0/utils/{block_before_timestamp_test.py => block_number_before_timestamp_test.py} (97%) diff --git a/pyproject.toml b/pyproject.toml index 9b0c8aabdb..f86141fded 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "fixedpointmath>=0.2.1", "hexbytes>=1.2.1", "hyperdrivepy==0.17.1", - "hyperdrivetypes==1.0.20.9", + "hyperdrivetypes==1.0.20.11", "ipython>=8.26.0", "ipykernel>=6.29.5", "matplotlib>=3.9.2", @@ -51,6 +51,7 @@ dependencies = [ "sqlalchemy>=2.0.32", "sqlalchemy-utils>=0.41.2", "streamlit>=1.37.1", + "tabulate>=0.9.0", "tqdm>=4.66.5", "web3>=7.3.0", ] diff --git a/src/agent0/core/hyperdrive/interactive/local_hyperdrive.py b/src/agent0/core/hyperdrive/interactive/local_hyperdrive.py index 23e099978e..bf32745962 100644 --- a/src/agent0/core/hyperdrive/interactive/local_hyperdrive.py +++ b/src/agent0/core/hyperdrive/interactive/local_hyperdrive.py @@ -318,7 +318,7 @@ def __init__( # At this point, we've deployed hyperdrive, so we want to save the block where it was deployed # for the data pipeline - self._deploy_block_number = self.interface.get_deploy_block() + self._deploy_block_number = self.interface.get_deploy_block_number() if deploy: # If we're deploying, we expect the deploy block to be set diff --git a/src/agent0/ethpy/base/transactions.py b/src/agent0/ethpy/base/transactions.py index 31af7482dd..5ff54c3919 100644 --- a/src/agent0/ethpy/base/transactions.py +++ b/src/agent0/ethpy/base/transactions.py @@ -5,7 +5,8 @@ import random from hexbytes import HexBytes -from pypechain.core import PypechainContractFunction, check_txn_receipt +from pypechain.core import PypechainContractFunction +from pypechain.core.contract_call_exception import check_txn_receipt from web3._utils.threads import Timeout from web3.exceptions import TimeExhausted, TransactionNotFound from web3.types import TxReceipt diff --git a/src/agent0/ethpy/hyperdrive/interface/read_interface.py b/src/agent0/ethpy/hyperdrive/interface/read_interface.py index 057783d23d..5534e28923 100644 --- a/src/agent0/ethpy/hyperdrive/interface/read_interface.py +++ b/src/agent0/ethpy/hyperdrive/interface/read_interface.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, cast import eth_abi +from eth_typing import BlockNumber from fixedpointmath import FixedPoint from hyperdrivetypes import CheckpointFP from hyperdrivetypes.types import ( @@ -85,7 +86,7 @@ if TYPE_CHECKING: from eth_account.signers.local import LocalAccount - from eth_typing import BlockNumber, ChecksumAddress + from eth_typing import ChecksumAddress AGENT0_SIGNATURE = bytes.fromhex("a0") @@ -290,18 +291,19 @@ def __init__( self.last_state_block_number = -1 # Cached deploy block - self._deploy_block: None | int = None + self._deploy_block: BlockNumber | None = None self._deploy_block_checked = False - def get_deploy_block(self) -> int | None: - """Get the block that the Hyperdrive contract was deployed on. + def get_deploy_block_number(self) -> BlockNumber | None: + """Get the block number that the Hyperdrive contract was deployed on. NOTE: The deploy event may get lost on e.g., anvil, so we ensure we only check once Returns ------- - int | None - The block that the Hyperdrive contract was deployed on. Returns None if it can't be found. + BlockNumber | None + The block number that the Hyperdrive contract was deployed on. + Returns None if it can't be found. """ if not self._deploy_block_checked: self._deploy_block_checked = True @@ -309,7 +311,6 @@ def get_deploy_block(self) -> int | None: # We look up the chain id, and define the `from_block` based on which chain it is as the default. chain_id = self.web3.eth.chain_id # If not in lookup, we default to `earliest` - # If not in lookup, we default to `earliest` if chain_id not in EARLIEST_BLOCK_LOOKUP: from_block = "earliest" else: @@ -319,7 +320,7 @@ def get_deploy_block(self) -> int | None: if len(initialize_event) == 0: logging.warning("Initialize event not found, can't set deploy_block") elif len(initialize_event) == 1: - self._deploy_block = initialize_event[0].block_number + self._deploy_block = BlockNumber(initialize_event[0].block_number) else: raise ValueError("Multiple initialize events found") diff --git a/src/agent0/hyperfuzz/fork_fuzz/accrue_interest_ezeth.py b/src/agent0/hyperfuzz/fork_fuzz/accrue_interest_ezeth.py index 0614599195..f252a914d9 100644 --- a/src/agent0/hyperfuzz/fork_fuzz/accrue_interest_ezeth.py +++ b/src/agent0/hyperfuzz/fork_fuzz/accrue_interest_ezeth.py @@ -4,7 +4,7 @@ from fixedpointmath import FixedPoint, FixedPointIntegerMath from hyperdrivetypes.types import IDepositQueueContract, IRestakeManagerContract -from pypechain.core import check_txn_receipt +from pypechain.core.contract_call_exception import check_txn_receipt from web3 import Web3 from web3.types import RPCEndpoint, TxParams, Wei diff --git a/src/agent0/hyperfuzz/system_fuzz/invariant_checks.py b/src/agent0/hyperfuzz/system_fuzz/invariant_checks.py index 6c811faab0..57b104fd4b 100644 --- a/src/agent0/hyperfuzz/system_fuzz/invariant_checks.py +++ b/src/agent0/hyperfuzz/system_fuzz/invariant_checks.py @@ -18,6 +18,7 @@ from agent0.ethpy.hyperdrive import HyperdriveReadInterface from agent0.ethpy.hyperdrive.state.pool_state import PoolState from agent0.hyperfuzz import FuzzAssertionException +from agent0.utils import block_number_before_timestamp LP_SHARE_PRICE_EPSILON = 1e-4 TOTAL_SHARES_EPSILON = 1e-9 @@ -283,71 +284,48 @@ def _check_negative_interest(interface: HyperdriveReadInterface, pool_state: Poo exception_data: dict[str, Any] = {} log_level = None - # We hack in a stateful variable into the interface here, since we need - # to check between subsequent calls here. - # TODO: build in a way to store old pool states, e.g. a dict keyed by block time - # Initial call, we look to see if the attribute exists - previous_pool_state: PoolState | None = getattr(interface, "_negative_interest_previous_pool_state", None) + current_block_time = pool_state.block_time + current_vault_share_price = pool_state.pool_info.vault_share_price - # We need to check interest over a longer time scale for ezETH + deploy_block = interface.get_deploy_block_number() + if deploy_block is None: # type narrowing + raise ValueError("Deploy block not found.") + deploy_block_time = interface.get_block_timestamp(interface.get_block(deploy_block)) if interface.hyperdrive_name == "ElementDAO 182 Day ezETH Hyperdrive": - if previous_pool_state is None: - # Set initial state - setattr(interface, "_negative_interest_previous_pool_state", pool_state) - else: - # Only set prev state if enough time has passed - if pool_state.block_time - previous_pool_state.block_time > EZETH_NEG_INTEREST_TIME_DELTA: - setattr(interface, "_negative_interest_previous_pool_state", pool_state) + lookback_timestamp = current_block_time - 60 * 60 * 12 # 12 hours ago else: - # Always set the new state for all other pools, or if prev state has not been set - setattr(interface, "_negative_interest_previous_pool_state", pool_state) - - if previous_pool_state is None: - # Skip this check on initial call, not a failure - return InvariantCheckResults( - failed=False, exception_message=exception_message, exception_data=exception_data, log_level=log_level - ) - - current_vault_share_price = pool_state.pool_info.vault_share_price + lookback_timestamp = current_block_time - 60 * 60 * 1 # 1 hour ago + if lookback_timestamp < deploy_block_time: + previous_block_number = deploy_block + else: + previous_block_number = block_number_before_timestamp(interface.web3, lookback_timestamp) + previous_pool_state = interface.get_hyperdrive_state(block_identifier=previous_block_number) previous_vault_share_price = previous_pool_state.pool_info.vault_share_price if (current_vault_share_price - previous_vault_share_price) <= -NEGATIVE_INTEREST_EPSILON: - exception_data["invariance_check:current_vault_share_price"] = current_vault_share_price - exception_data["invariance_check:previous_vault_share_price"] = previous_vault_share_price - failed = True - # Different error messages and log levels if the pool is paused if interface.get_pool_is_paused(): - exception_message = ( - "Negative interest detected between block " - f"{previous_pool_state.block_number} " - "at time " - f"{previous_pool_state.block_time} " - "and block " - f"{pool_state.block_number} " - "at time " - f"{pool_state.block_time} " - "on paused pool. " - f"{current_vault_share_price=}, {previous_vault_share_price=}. " - "Difference in wei: " - f"{current_vault_share_price.scaled_value - previous_vault_share_price.scaled_value}." - ) + paused_str = "paused" log_level = logging.WARNING else: - exception_message = ( - "Negative interest detected beteween block " - f"{previous_pool_state.block_number} " - "at time " - f"{previous_pool_state.block_time} " - "and block " - f"{pool_state.block_number} " - "at time " - f"{pool_state.block_time} " - "on unpaused pool. " - f"{current_vault_share_price=}, {previous_vault_share_price=}. " - "Difference in wei: " - f"{current_vault_share_price.scaled_value - previous_vault_share_price.scaled_value}." - ) + paused_str = "unpaused" log_level = logging.CRITICAL + failed = True + exception_data["invariance_check:current_vault_share_price"] = current_vault_share_price + exception_data["invariance_check:previous_vault_share_price"] = previous_vault_share_price + exception_message = ( + "Negative interest detected beteween block " + f"{previous_pool_state.block_number} " + "at time " + f"{previous_pool_state.block_time} " + "and block " + f"{pool_state.block_number} " + "at time " + f"{pool_state.block_time} " + f"on {paused_str} pool. " + f"{current_vault_share_price=}, {previous_vault_share_price=}. " + "Difference in wei: " + f"{current_vault_share_price.scaled_value - previous_vault_share_price.scaled_value}." + ) return InvariantCheckResults(failed, exception_message, exception_data, log_level=log_level) diff --git a/src/agent0/utils/__init__.py b/src/agent0/utils/__init__.py index 957bc30591..6393376802 100644 --- a/src/agent0/utils/__init__.py +++ b/src/agent0/utils/__init__.py @@ -1,4 +1,9 @@ """General utility functions""" from .async_runner import async_runner -from .block_before_timestamp import block_number_before_timestamp +from .block_number_before_timestamp import block_number_before_timestamp + +__all__ = [ + "async_runner", + "block_number_before_timestamp", +] diff --git a/src/agent0/utils/block_before_timestamp.py b/src/agent0/utils/block_number_before_timestamp.py similarity index 100% rename from src/agent0/utils/block_before_timestamp.py rename to src/agent0/utils/block_number_before_timestamp.py diff --git a/src/agent0/utils/block_before_timestamp_test.py b/src/agent0/utils/block_number_before_timestamp_test.py similarity index 97% rename from src/agent0/utils/block_before_timestamp_test.py rename to src/agent0/utils/block_number_before_timestamp_test.py index 4de30bf807..9287047567 100644 --- a/src/agent0/utils/block_before_timestamp_test.py +++ b/src/agent0/utils/block_number_before_timestamp_test.py @@ -5,7 +5,7 @@ from agent0 import LocalChain, LocalHyperdrive -from .block_before_timestamp import block_number_before_timestamp +from .block_number_before_timestamp import block_number_before_timestamp @pytest.mark.docker