Skip to content

Commit

Permalink
add block_number_before_timestamp; chain.mine(); ezeth price chec…
Browse files Browse the repository at this point in the history
…k script (#1728)
  • Loading branch information
dpaiton authored Nov 5, 2024
1 parent 2fdeb6a commit cc891d9
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 4 deletions.
78 changes: 78 additions & 0 deletions scripts/check_ezeth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Script to check the ezeth vault share price."""

import argparse
import os

from dotenv import load_dotenv

from agent0 import Chain, Hyperdrive
from agent0.utils import block_number_before_timestamp

load_dotenv(".env")
DEV_RPC_URI = os.getenv("DEV_RPC_URI", "")
RPC_URI = DEV_RPC_URI
REGISTRY_ADDRESS = os.getenv("REGISTRY_ADDRESS", "")


def main(start_block_timestamp: int, lookback_length: int):
"""Main entry point.
Arguments
---------
start_block_timestamp: int
The start block timestamp, in seconds (should be after the ezETH Hyperdrive pool was deployed).
lookback_length: int
The number of seconds to lookback from the start block.
"""
with Chain(RPC_URI, config=Chain.Config(no_postgres=True)) as chain:
# Get the ezeth pool
pool_name = "ElementDAO 182 Day ezETH Hyperdrive"
registered_pools = Hyperdrive.get_hyperdrive_pools_from_registry(
chain,
registry_address=REGISTRY_ADDRESS,
)
ezeth_pool = [pool for pool in registered_pools if pool.name == pool_name][0]
web3 = ezeth_pool.interface.web3

# If the start block is zero, use the current block
if start_block_timestamp <= 0:
start_block_timestamp = chain.block_time()
start_block_number = chain.block_number()
# Otherwise, find the block with the given blocktime
else:
start_block_number = block_number_before_timestamp(web3, start_block_timestamp)

start_pool_state = ezeth_pool.interface.get_hyperdrive_state(block_identifier=start_block_number)
start_vault_share_price = start_pool_state.pool_info.vault_share_price

lookback_timestamp = start_block_timestamp - lookback_length
lookback_block_number = block_number_before_timestamp(web3, lookback_timestamp)
lookback_pool_state = ezeth_pool.interface.get_hyperdrive_state(block_identifier=lookback_block_number)
lookback_vault_share_price = lookback_pool_state.pool_info.vault_share_price

print(
f"Calculating vault share price difference between block {start_block_number} and block {lookback_block_number}"
)
print(f"time = {start_block_timestamp}; vault share price = {start_vault_share_price}")
print(f"time = {lookback_timestamp}; vault share price = {lookback_vault_share_price}")
print(f"Difference: {start_vault_share_price - lookback_vault_share_price=}")
if start_vault_share_price < lookback_vault_share_price:
print("WARNING: NEGATIVE INTEREST!")


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Check EZETH pool vault share price difference.")
parser.add_argument(
"--start-block",
type=int,
default=0, # 0 will use current block.
help="The starting block number.",
)
parser.add_argument(
"--lookback",
type=int,
default=60 * 60 * 12, # 12 hours
help="The number of blocks to lookback from the starting block.",
)
args = parser.parse_args()
main(args.start_block, args.lookback)
3 changes: 2 additions & 1 deletion src/agent0/core/hyperdrive/interactive/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from docker import DockerClient
from docker.errors import NotFound
from docker.models.containers import Container
from eth_typing import BlockNumber
from numpy.random import Generator
from web3.types import BlockData, BlockIdentifier, Timestamp

Expand Down Expand Up @@ -342,7 +343,7 @@ def cleanup(self):
except Exception: # pylint: disable=broad-except
pass

def block_number(self) -> int:
def block_number(self) -> BlockNumber:
"""Get the current block number on the chain.
Returns
Expand Down
29 changes: 28 additions & 1 deletion src/agent0/core/hyperdrive/interactive/local_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,33 @@ def _set_block_timestamp_interval(self, timestamp_interval: int) -> None:
if "result" not in response:
raise KeyError("Response did not have a result.")

def _mine_chain_blocks(self, num_blocks: int = 1) -> None:
response = self._web3.provider.make_request(method=RPCEndpoint("anvil_mine"), params=[num_blocks])
# ensure response is valid
if "result" not in response:
raise KeyError("Response did not have a result.")

def mine_blocks(self, num_blocks: int = 1) -> None:
"""Advance time for this chain using the `anvil_mine` RPC call.
This function mines the specified amount of blocks with the chain config
specified time between blocks.
.. note::
This advances the chain for all pool connected to this chain.
.. todo::
Add support for minting checkpoints.
Arguments
---------
num_blocks: int
The amount of blocks to advance. Defaults to 1.
"""
self._mine_chain_blocks(num_blocks)
for pool in self._deployed_hyperdrive_pools:
pool._maybe_run_blocking_data_pipeline() # pylint: disable=protected-access

# pylint: disable=too-many-branches
def advance_time(
self, time_delta: int | timedelta, create_checkpoints: bool = True
Expand All @@ -292,7 +319,7 @@ def advance_time(
This function looks at the timestamp of the current block, then
mines a block explicitly setting the timestamp to the current block timestamp + time_delta.
If create_checkpoints is True, it will also create intermediate when advancing time.
If create_checkpoints is True, it will also create intermediate checkpoints when advancing time.
.. note::
This advances the chain for all pool connected to this chain.
Expand Down
44 changes: 44 additions & 0 deletions src/agent0/core/hyperdrive/interactive/local_chain_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Tests for convenience functions in the Local Chain."""

import numpy as np
import pytest

from agent0 import LocalChain


@pytest.mark.docker
@pytest.mark.anvil
def test_mine_blocks(fast_chain_fixture: LocalChain):
"""Test that ensures block_before_timestamp always returns a block number
that is closest to but before the timestamp.
"""

num_fuzz_runs = 50

time_between_blocks = fast_chain_fixture.config.block_timestamp_interval
assert time_between_blocks is not None

previous_block_number = fast_chain_fixture.block_number()
previous_block_time = fast_chain_fixture.block_time()

for _ in range(num_fuzz_runs):
# Advance the chain a random number of blocks
num_blocks = np.random.randint(1, 1_000)
fast_chain_fixture.mine_blocks(num_blocks)

# Get the new block number and time
current_block_number = fast_chain_fixture.block_number()
current_block_time = fast_chain_fixture.block_time()
avg_time_between_blocks = (current_block_time - previous_block_time) / num_blocks

# Check that we advanced the blocks correctly
assert (
current_block_number - previous_block_number == num_blocks
), f"{current_block_number-previous_block_number=} != {num_blocks=}"
assert (
current_block_time - previous_block_time == num_blocks * time_between_blocks
), f"{current_block_time-previous_block_time=} != {num_blocks * time_between_blocks=}"
assert avg_time_between_blocks == time_between_blocks, f"{avg_time_between_blocks=} != {time_between_blocks=}"

previous_block_number = current_block_number
previous_block_time = current_block_time
20 changes: 18 additions & 2 deletions src/agent0/hyperfuzz/system_fuzz/invariant_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,15 +318,31 @@ def _check_negative_interest(interface: HyperdriveReadInterface, pool_state: Poo
# Different error messages and log levels if the pool is paused
if interface.get_pool_is_paused():
exception_message = (
"Negative interest detected on paused pool. "
"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}."
)
log_level = logging.WARNING
else:
exception_message = (
"Negative interest detected on unpaused pool. "
"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}."
Expand Down
1 change: 1 addition & 0 deletions src/agent0/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""General utility functions"""

from .async_runner import async_runner
from .block_before_timestamp import block_number_before_timestamp
88 changes: 88 additions & 0 deletions src/agent0/utils/block_before_timestamp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Get the number of the block that is at or immediately before the given timestamp."""

from __future__ import annotations

from eth_typing import BlockNumber
from web3 import Web3
from web3.types import Timestamp

# pylint: disable=too-many-locals


def block_number_before_timestamp(web3: Web3, block_timestamp: Timestamp | int) -> BlockNumber:
"""Finds the closest block number that is before or at the given block time.
Arguments
---------
web3: Web3
The web3 instance.
block_timestamp: BlockTime | int
The block time to find the closest block to.
Returns
-------
BlockNumber
The closest block number to the given block time.
"""
# Get the current block number and timestamp
block_timestamp = Timestamp(block_timestamp)
current_block = web3.eth.get_block("latest")
current_block_number = current_block.get("number", None)
assert current_block_number is not None
if current_block_number < 3:
raise ValueError("The current block number must be >= 2.")
current_block_timestamp = current_block.get("timestamp", None)
assert current_block_timestamp is not None

# Estimate the average block time
earlier_block_number = current_block_number // 2
earlier_block_delta = current_block_number - earlier_block_number
if earlier_block_delta <= 0:
raise ValueError("Error estimating the delta blocks.")
earlier_block_timestamp = web3.eth.get_block(earlier_block_number).get("timestamp", None)
assert earlier_block_timestamp is not None
avg_time_between_blocks = (current_block_timestamp - earlier_block_timestamp) / earlier_block_delta

# Estimate the block number corresponding to the user provided timestamp
delta_time = current_block_timestamp - block_timestamp
estimated_block_number = current_block_number - (delta_time // avg_time_between_blocks)

# Establish upper and lower bounds
left = int(max(0, estimated_block_number - 100)) # search 100 blocks before estimated block
right = int(min(current_block_number, estimated_block_number + 100)) # search 100 blocks after estimated block

# Ensure bounds of binary search is within the target timestamp
left_timestamp = web3.eth.get_block(left).get("timestamp", None)
assert left_timestamp is not None
if left_timestamp > block_timestamp:
left = 0
right_timestamp = web3.eth.get_block(right).get("timestamp", None)
assert right_timestamp is not None
if right_timestamp < block_timestamp:
right = current_block_number

# Use a binary search to find the block
while left <= right:
mid = int((left + right) // 2)
block = web3.eth.get_block(BlockNumber(mid))
search_block_timestamp = block.get("timestamp", None)
assert search_block_timestamp is not None
if search_block_timestamp > block_timestamp:
# The mid point is later than the block we want, set right to mid-1
right = mid - 1
elif search_block_timestamp < block_timestamp:
# The mid point is earlier than the block we want
# The user could enter a time that is greater than the nearest block
# timestamp but less than the next block timestamp
next_block = web3.eth.get_block(BlockNumber(mid + 1))
next_block_timestamp = next_block.get("timestamp", None)
assert next_block_timestamp is not None
if next_block_timestamp > block_timestamp:
# The user time is between the current and next block
return BlockNumber(mid)
# If the next block wasn't right, then set it to left
left = mid + 1
else:
# The mid point is the right block
return BlockNumber(mid)
return BlockNumber(left)
59 changes: 59 additions & 0 deletions src/agent0/utils/block_before_timestamp_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Test for the block_before_timestamp function."""

import numpy as np
import pytest

from agent0 import LocalChain, LocalHyperdrive

from .block_before_timestamp import block_number_before_timestamp


@pytest.mark.docker
@pytest.mark.anvil
def test_block_number_before_timestamp(fast_chain_fixture: LocalChain):
"""Test that ensures block_before_timestamp always returns a block number
with a timestamp that is closest to but before the provided timestamp.
"""
# Run the test a bunch of times because the function is iterative.
num_fuzz_runs = 2_000

# Advance the chain NUM_FUZZ_RUNS blocks
initial_block_number = fast_chain_fixture.block_number()
initial_block_time = fast_chain_fixture.block_time()

fast_chain_fixture.mine_blocks(1)
initial_block_plus_one_time = fast_chain_fixture.block_time()

time_between_blocks = initial_block_plus_one_time - initial_block_time
assert time_between_blocks >= 1
fast_chain_fixture.mine_blocks(num_fuzz_runs - 1)
chain_block_number = fast_chain_fixture.block_number()
assert chain_block_number == initial_block_number + num_fuzz_runs

# Start out a few blocks behind the latest
hyperdrive_interface = LocalHyperdrive(fast_chain_fixture, LocalHyperdrive.Config()).interface
for fuzz_iter in range(num_fuzz_runs):
# Grab a random block in the past & get the time
if fuzz_iter == 0: # test an edge case on the first iteration
test_block_number = initial_block_number + 3
elif fuzz_iter == 1: # test an edge case on the second iteration
test_block_number = chain_block_number - 1
else:
test_block_number = int(np.random.randint(initial_block_number + 3, chain_block_number - 1))
test_block_time = hyperdrive_interface.get_block_timestamp(hyperdrive_interface.get_block(test_block_number))

# Add a random amount of time that is less than the time between blocks
time_delta = int(np.random.randint(0, time_between_blocks))

# Find the block that was closest to this timestamp
inferred_block_number = block_number_before_timestamp(hyperdrive_interface.web3, test_block_time + time_delta)
inferred_block_time = hyperdrive_interface.get_block_timestamp(
hyperdrive_interface.get_block(inferred_block_number)
)

assert (
inferred_block_number == test_block_number
), f"Inferred block number = {inferred_block_number} should be less than or equal to {test_block_number=}."
assert (
inferred_block_time <= test_block_time
), f"{inferred_block_time=} should be less than or equal to {test_block_time=}."

0 comments on commit cc891d9

Please sign in to comment.