Skip to content

Commit

Permalink
Refork periodically (#1711)
Browse files Browse the repository at this point in the history
Minor fixes:
- Bugfix to random policy to not attempt to remove liquidity if the
amount of LP the wallet contains is less than the minimum transaction
amount.
- Bugfix for minimum transaction amounts in the case where agent0 is
dealing with share amounts.
- Adding rollbar ignores for fork fuzzing to avoid spamming rollbar.
  • Loading branch information
slundqui authored Oct 22, 2024
1 parent a95e72e commit 438a56c
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 46 deletions.
83 changes: 61 additions & 22 deletions scripts/fork_fuzz_bots.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,37 @@
}


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, FuzzAssertionException):
# Large circuit breaker check
if (
len(exc.args) >= 2
and exc.args[0] == "Continuous Fuzz Bots Invariant Checks"
and "Large trade has caused the rate circuit breaker to trip." in exc.args[1]
):
return True
elif 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 +249,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 +300,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 +326,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 +350,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 +415,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
19 changes: 17 additions & 2 deletions scripts/local_fuzz_bots.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ def _fuzz_ignore_logging_to_rollbar(exc: Exception) -> bool:
known issues due to random bots not accounting for these cases, so we don't log them to
rollbar.
"""
if isinstance(exc, PypechainCallException):
if isinstance(exc, FuzzAssertionException):
# Large circuit breaker check
if (
len(exc.args) >= 2
and exc.args[0] == "Continuous Fuzz Bots Invariant Checks"
and "Large trade has caused the rate circuit breaker to trip." in exc.args[1]
):
return True
elif isinstance(exc, PypechainCallException):
orig_exception = exc.orig_exception
if orig_exception is None:
return False
Expand Down Expand Up @@ -234,7 +242,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 +270,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 +294,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 +354,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 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

# 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

0 comments on commit 438a56c

Please sign in to comment.