Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refork periodically #1711

Merged
merged 6 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 53 additions & 22 deletions scripts/fork_fuzz_bots.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@
}


def _fuzz_ignore_logging_to_rollbar(exc: Exception) -> bool:
"""Function defining errors to not log to rollbar during fuzzing.

These are the two most common errors we see in local fuzz testing. These are
known issues due to random bots not accounting for these cases, so we don't log them to
rollbar.
"""
if isinstance(exc, PypechainCallException):
orig_exception = exc.orig_exception
if orig_exception is None:
return False

# Insufficient liquidity error
if isinstance(orig_exception, ContractCustomError) and exc.decoded_error == "InsufficientLiquidity()":
return True

# Circuit breaker triggered error
if isinstance(orig_exception, ContractCustomError) and exc.decoded_error == "CircuitBreakerTriggered()":
return True

return False


def _fuzz_ignore_errors(exc: Exception) -> bool:
"""Function defining errors to ignore for pausing chain during fuzzing."""
# pylint: disable=too-many-return-statements
Expand Down Expand Up @@ -218,41 +241,41 @@ def main(argv: Sequence[str] | None = None) -> None:
preview_before_trade=True,
log_to_rollbar=log_to_rollbar,
rollbar_log_prefix="forkfuzzbots",
rollbar_log_filter_func=_fuzz_ignore_logging_to_rollbar,
rng=rng,
crash_log_level=logging.ERROR,
crash_report_additional_info={"rng_seed": rng_seed},
gas_limit=int(1e6), # Plenty of gas limit for transactions
)
# Build interactive local hyperdrive
chain = LocalChain(fork_uri=rpc_uri, config=chain_config)

chain_id = chain.chain_id
# Select whale account based on chain id
if chain_id in WHALE_ADDRESSES:
# Ensure all whale account addresses are checksum addresses
whale_accounts = {
Web3.to_checksum_address(key): Web3.to_checksum_address(value)
for key, value in WHALE_ADDRESSES[chain_id].items()
}
else:
whale_accounts = {}

# Get list of deployed pools on initial iteration
deployed_pools = LocalHyperdrive.get_hyperdrive_pools_from_registry(chain, registry_address)
log_message = f"Running fuzzing on pools {[p.name for p in deployed_pools]}..."
logging.info(log_message)
log_rollbar_message(message=log_message, log_level=logging.INFO)

while True:
# Build interactive local hyperdrive
chain = LocalChain(fork_uri=rpc_uri, config=chain_config)

chain_id = chain.chain_id
# Select whale account based on chain id
if chain_id in WHALE_ADDRESSES:
# Ensure all whale account addresses are checksum addresses
whale_accounts = {
Web3.to_checksum_address(key): Web3.to_checksum_address(value)
for key, value in WHALE_ADDRESSES[chain_id].items()
}
else:
whale_accounts = {}

# Get list of deployed pools on initial iteration
deployed_pools = LocalHyperdrive.get_hyperdrive_pools_from_registry(chain, registry_address)
log_message = f"Running fuzzing on pools {[p.name for p in deployed_pools]}..."
logging.info(log_message)
log_rollbar_message(message=log_message, log_level=logging.INFO)
# Check for new pools
latest_block = chain.block_data()
latest_block_number = latest_block.get("number", None)
if latest_block_number is None:
raise AssertionError("Block has no number.")

# TODO we may want to refork every once in awhile in case new pools are deployed.
# For now, we assume this script gets restarted.

# We run fuzzbots for every num_iterations_per_episode,
# after which we will refork and restart.
try:
run_fuzz_bots(
chain,
Expand All @@ -269,6 +292,7 @@ def main(argv: Sequence[str] | None = None) -> None:
lp_share_price_test=False,
base_budget_per_bot=FixedPoint(1_000),
whale_accounts=whale_accounts,
num_iterations=parsed_args.num_iterations_per_episode,
)
except Exception as e: # pylint: disable=broad-except
log_rollbar_exception(exception=e, log_level=logging.ERROR)
Expand All @@ -294,6 +318,7 @@ class Args(NamedTuple):
db_port: int
rpc_uri: str
rng_seed: int
num_iterations_per_episode: int


def namespace_to_args(namespace: argparse.Namespace) -> Args:
Expand All @@ -317,6 +342,7 @@ def namespace_to_args(namespace: argparse.Namespace) -> Args:
db_port=namespace.db_port,
rpc_uri=namespace.rpc_uri,
rng_seed=namespace.rng_seed,
num_iterations_per_episode=namespace.num_iterations_per_episode,
)


Expand Down Expand Up @@ -381,6 +407,11 @@ def parse_arguments(argv: Sequence[str] | None = None) -> Args:
default=-1,
help="The random seed to use for the fuzz run.",
)
parser.add_argument(
"--num-iterations-per-episode",
default=3000,
help="The number of iterations to run for each random pool config.",
)

# Use system arguments if none were passed
if argv is None:
Expand Down
9 changes: 8 additions & 1 deletion scripts/local_fuzz_bots.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def main(argv: Sequence[str] | None = None) -> None:
run_async=False,
random_advance_time=True,
random_variable_rate=True,
num_iterations=3000,
num_iterations=parsed_args.num_iterations_per_episode,
lp_share_price_test=parsed_args.lp_share_price_test,
)

Expand Down Expand Up @@ -262,6 +262,7 @@ class Args(NamedTuple):
genesis_timestamp: int
rng_seed: int
steth: bool
num_iterations_per_episode: int


def namespace_to_args(namespace: argparse.Namespace) -> Args:
Expand All @@ -285,6 +286,7 @@ def namespace_to_args(namespace: argparse.Namespace) -> Args:
genesis_timestamp=namespace.genesis_timestamp,
rng_seed=namespace.rng_seed,
steth=namespace.steth,
num_iterations_per_episode=namespace.num_iterations_per_episode,
)


Expand Down Expand Up @@ -344,6 +346,11 @@ def parse_arguments(argv: Sequence[str] | None = None) -> Args:
action="store_true",
help="Runs fuzz testing on the steth hyperdrive",
)
parser.add_argument(
"--num-iterations-per-episode",
default=3000,
help="The number of iterations to run for each random pool config.",
)

# Use system arguments if none were passed
if argv is None:
Expand Down
54 changes: 38 additions & 16 deletions src/agent0/core/hyperdrive/policies/random.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,16 @@ def get_available_actions(
A list containing all of the available actions.
"""
pool_state = interface.current_pool_state
# The amount of minimum transaction amount is dependent on if we're trading with
slundqui marked this conversation as resolved.
Show resolved Hide resolved
# base or vault shares
if interface.base_is_yield:
minimum_transaction_amount = interface.get_minimum_transaction_amount_shares()
else:
minimum_transaction_amount = pool_state.pool_config.minimum_transaction_amount

# prevent accidental override
# compile a list of all actions
if wallet.balance.amount <= pool_state.pool_config.minimum_transaction_amount:
if wallet.balance.amount <= minimum_transaction_amount:
all_available_actions = []
else:
all_available_actions = [
Expand All @@ -127,7 +134,9 @@ def get_available_actions(
all_available_actions.append(HyperdriveActionType.CLOSE_LONG)
if wallet.shorts: # if the agent has open shorts
all_available_actions.append(HyperdriveActionType.CLOSE_SHORT)
if wallet.lp_tokens:
# If the agent has more than minimum transaction amount of liquidity to remove
# Note the lp tokens are always bounded by the actual minimum share reserves in pool config
if wallet.lp_tokens >= pool_state.pool_config.minimum_transaction_amount:
all_available_actions.append(HyperdriveActionType.REMOVE_LIQUIDITY)
if wallet.withdraw_shares and pool_state.pool_info.withdrawal_shares_ready_to_withdraw > 0:
all_available_actions.append(HyperdriveActionType.REDEEM_WITHDRAW_SHARE)
Expand All @@ -151,6 +160,10 @@ def open_short_with_random_amount(
list[Trade[HyperdriveMarketAction]]
A list with a single Trade element for opening a Hyperdrive short.
"""
# Shorts take units of bonds, which is always checked against the minimum transaction amount
# as defined in pool config.
minimum_transaction_amount = interface.pool_config.minimum_transaction_amount

# Calc max short is crashing, we surround in try catch to log
# TODO fix all crashes in calc_max_short and calc_max_long and instead return 0 for max short
try:
Expand Down Expand Up @@ -183,16 +196,14 @@ def open_short_with_random_amount(
# We don't return a trade here if this fails
return []

if maximum_trade_amount <= interface.pool_config.minimum_transaction_amount:
if maximum_trade_amount <= minimum_transaction_amount:
return []

initial_trade_amount = FixedPoint(
self.rng.normal(loc=float(wallet.balance.amount) * 0.1, scale=float(wallet.balance.amount) * 0.01)
)
# minimum_transaction_amount <= trade_amount <= max_short
trade_amount = max(
interface.pool_config.minimum_transaction_amount, min(initial_trade_amount, maximum_trade_amount)
)
trade_amount = max(minimum_transaction_amount, min(initial_trade_amount, maximum_trade_amount))
# optionally ignore slippage tolerance
ignore_slippage = self.rng.choice([True, False], size=1) if self.randomly_ignore_slippage_tolerance else False
if ignore_slippage:
Expand Down Expand Up @@ -261,6 +272,15 @@ def open_long_with_random_amount(
list[Trade[HyperdriveMarketAction]]
A list with a single Trade element for opening a Hyperdrive long.
"""

pool_state = interface.current_pool_state
# open long's minimum transaction amount is dependent on if we're trading with
# base or vault shares
if interface.base_is_yield:
minimum_transaction_amount = interface.get_minimum_transaction_amount_shares()
else:
minimum_transaction_amount = pool_state.pool_config.minimum_transaction_amount

# TODO fix all crashes in calc_max_short and calc_max_long and instead return 0 for max short
try:
maximum_trade_amount = interface.calc_max_long(wallet.balance.amount, interface.current_pool_state)
Expand Down Expand Up @@ -290,16 +310,14 @@ def open_long_with_random_amount(
)
# We don't return a trade here if this fails
return []
if maximum_trade_amount <= interface.pool_config.minimum_transaction_amount:
if maximum_trade_amount <= minimum_transaction_amount:
return []
# take a guess at the trade amount, which should be about 10% of the agent’s budget
initial_trade_amount = FixedPoint(
self.rng.normal(loc=float(wallet.balance.amount) * 0.1, scale=float(wallet.balance.amount) * 0.01)
)
# minimum_transaction_amount <= trade_amount <= max long
trade_amount = max(
interface.pool_config.minimum_transaction_amount, min(initial_trade_amount, maximum_trade_amount)
)
trade_amount = max(minimum_transaction_amount, min(initial_trade_amount, maximum_trade_amount))
# optionally ignore slippage tolerance
ignore_slippage = self.rng.choice([True, False], size=1) if self.randomly_ignore_slippage_tolerance else False
if ignore_slippage:
Expand Down Expand Up @@ -368,14 +386,16 @@ def add_liquidity_with_random_amount(
list[Trade[HyperdriveMarketAction]]
A list with a single Trade element for adding liquidity to a Hyperdrive pool.
"""
# LP is in units of LP, which is always checked against the minimum transaction amount
# as defined in pool config.
minimum_transaction_amount = interface.pool_config.minimum_transaction_amount

# take a guess at the trade amount, which should be about 10% of the agent’s budget
initial_trade_amount = FixedPoint(
self.rng.normal(loc=float(wallet.balance.amount) * 0.1, scale=float(wallet.balance.amount) * 0.01)
)
# minimum_transaction_amount <= trade_amount
trade_amount: FixedPoint = max(
interface.pool_config.minimum_transaction_amount, min(wallet.balance.amount, initial_trade_amount)
)
trade_amount: FixedPoint = max(minimum_transaction_amount, min(wallet.balance.amount, initial_trade_amount))
# return a trade using a specification that is parsable by the rest of the sim framework
return [
add_liquidity_trade(
Expand All @@ -400,14 +420,16 @@ def remove_liquidity_with_random_amount(
list[Trade[HyperdriveMarketAction]]
A list with a single Trade element for removing liquidity from a Hyperdrive pool.
"""
# LP is in units of LP, which is always checked against the minimum transaction amount
# as defined in pool config.
minimum_transaction_amount = interface.pool_config.minimum_transaction_amount

# take a guess at the trade amount, which should be about 10% of the agent’s budget
initial_trade_amount = FixedPoint(
self.rng.normal(loc=float(wallet.balance.amount) * 0.1, scale=float(wallet.balance.amount) * 0.01)
)
# minimum_transaction_amount <= trade_amount <= lp_tokens
trade_amount = max(
interface.pool_config.minimum_transaction_amount, min(wallet.lp_tokens, initial_trade_amount)
)
trade_amount = max(minimum_transaction_amount, min(wallet.lp_tokens, initial_trade_amount))
# optionally ignore slippage tolerance
ignore_slippage = self.rng.choice([True, False], size=1) if self.randomly_ignore_slippage_tolerance else False
if ignore_slippage:
Expand Down
13 changes: 10 additions & 3 deletions src/agent0/core/hyperdrive/policies/random_hold.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
if TYPE_CHECKING:
from agent0.core.hyperdrive import HyperdriveMarketAction, HyperdriveWallet, TradeResult
from agent0.ethpy.hyperdrive import HyperdriveReadInterface
from agent0.ethpy.hyperdrive.state import PoolState


class RandomHold(Random):
Expand Down Expand Up @@ -121,6 +120,12 @@ def get_available_actions(
"""
# pylint: disable=too-many-branches
pool_state = interface.current_pool_state
# The amount of minimum transaction amount is dependent on if we're trading with
# base or vault shares
if interface.base_is_yield:
minimum_transaction_amount = interface.get_minimum_transaction_amount_shares()
else:
minimum_transaction_amount = pool_state.pool_config.minimum_transaction_amount

# Initialize list of open positions
if interface.hyperdrive_address not in self.open_positions:
Expand All @@ -143,7 +148,7 @@ def get_available_actions(
# Sanity check
raise ValueError(f"Action type {position.action_type} not in allowable actions")

if wallet.balance.amount <= pool_state.pool_config.minimum_transaction_amount:
if wallet.balance.amount <= minimum_transaction_amount:
all_available_actions = []
else:
all_available_actions = [
Expand All @@ -161,7 +166,9 @@ def get_available_actions(
all_available_actions.append(HyperdriveActionType.CLOSE_LONG)
if short_ready_to_close: # if the agent has shorts ready to close
all_available_actions.append(HyperdriveActionType.CLOSE_SHORT)
if wallet.lp_tokens:
# If the agent has more than minimum transaction amount of liquidity to remove
# Note the lp tokens are always bounded by the actual minimum share reserves in pool config
if wallet.lp_tokens >= pool_state.pool_config.minimum_transaction_amount:
all_available_actions.append(HyperdriveActionType.REMOVE_LIQUIDITY)
if wallet.withdraw_shares and pool_state.pool_info.withdrawal_shares_ready_to_withdraw > 0:
all_available_actions.append(HyperdriveActionType.REDEEM_WITHDRAW_SHARE)
Expand Down
17 changes: 17 additions & 0 deletions src/agent0/ethpy/hyperdrive/interface/_contract_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@
# pylint: disable=too-many-positional-arguments


def _get_minimum_transaction_amount_shares(
interface: HyperdriveReadInterface,
hyperdrive_contract: IHyperdriveContract,
block_identifier: BlockIdentifier | None = None,
) -> FixedPoint:
# Get the minimum transaction amount in units of base
minimum_transaction_amount_base = interface.pool_config.minimum_transaction_amount
# Convert to shares via rpc call, and cast as fixed point
return FixedPoint(
scaled_value=hyperdrive_contract.functions.convertToShares(minimum_transaction_amount_base.scaled_value).call(
block_identifier=block_identifier or "latest"
)
)


def _get_total_supply_withdrawal_shares(
hyperdrive_contract: IHyperdriveContract, block_identifier: BlockIdentifier | None = None
) -> FixedPoint:
Expand Down Expand Up @@ -79,6 +94,8 @@ def _get_vault_shares(
block_identifier: BlockIdentifier | None = None,
) -> FixedPoint:
"""See API for documentation."""

# TODO call `hyperdrive_contract.functions.totalShares` instead of custom logic between pools
if interface.hyperdrive_kind == interface.HyperdriveKind.STETH:
# Type narrowing
assert interface.vault_shares_token_contract is not None
Expand Down
Loading
Loading