Skip to content

Commit

Permalink
fix invariant check to not require stateful variables (#1731)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dpaiton authored Nov 12, 2024
1 parent 5cd827e commit a75a091
Show file tree
Hide file tree
Showing 9 changed files with 55 additions and 69 deletions.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
]
Expand Down
2 changes: 1 addition & 1 deletion src/agent0/core/hyperdrive/interactive/local_hyperdrive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/agent0/ethpy/base/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions src/agent0/ethpy/hyperdrive/interface/read_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -290,26 +291,26 @@ 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

# 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:
Expand All @@ -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")

Expand Down
2 changes: 1 addition & 1 deletion src/agent0/hyperfuzz/fork_fuzz/accrue_interest_ezeth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
88 changes: 33 additions & 55 deletions src/agent0/hyperfuzz/system_fuzz/invariant_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
7 changes: 6 additions & 1 deletion src/agent0/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit a75a091

Please sign in to comment.