diff --git a/tests/unitary/pool/stateful/legacy/stateful_base.py b/tests/unitary/pool/stateful/legacy/stateful_base.py deleted file mode 100644 index e8769d2..0000000 --- a/tests/unitary/pool/stateful/legacy/stateful_base.py +++ /dev/null @@ -1,237 +0,0 @@ -import contextlib -from math import log - -import boa -from boa.test import strategy as boa_st -from hypothesis import strategies as hyp_st -from hypothesis.stateful import RuleBasedStateMachine, invariant, rule - -from tests.fixtures.pool import INITIAL_PRICES -from tests.utils.tokens import mint_for_testing - -MAX_D = 10**12 * 10**18 # $1T is hopefully a reasonable cap for tests - - -class StatefulBase(RuleBasedStateMachine): - # strategy to pick two random amount for the two tokens - # in the pool. Useful for depositing, withdrawing, etc. - two_token_amounts = boa_st("uint256[2]", min_value=0, max_value=10**9 * 10**18) - - # strategy to pick a random amount for an action like exchange amounts, - # remove_liquidity (to determine the LP share), - # remove_liquidity_one_coin, etc. - token_amount = boa_st("uint256", max_value=10**12 * 10**18) - - # exchange_amount_in = strategy("uint256", max_value=10**9 * 10**18) - - # strategy to pick which token should be exchanged for the other - exchange_i = boa_st("uint8", max_value=1) - - # strategy to decide by how much we should move forward in time - # for ramping, oracle updates, etc. - sleep_time = boa_st("uint256", max_value=86400 * 7) - - # strategy to pick which address should perform the action - user = boa_st("address") - - percentage = hyp_st.integers(min_value=1, max_value=100).map(lambda x: x / 100) - - def __init__(self): - super().__init__() - - self.decimals = [int(c.decimals()) for c in self.coins] - self.user_balances = {u: [0] * 2 for u in self.users} - self.initial_prices = INITIAL_PRICES - self.initial_deposit = [ - 10**4 * 10 ** (18 + d) // p for p, d in zip(self.initial_prices, self.decimals) - ] # $10k * 2 - - self.xcp_profit = 10**18 - self.xcp_profit_a = 10**18 - - self.total_supply = 0 - self.previous_pool_profit = 0 - - self.swap_admin = self.swap.admin() - self.fee_receiver = self.swap.fee_receiver() - - for user in self.users: - for coin in self.coins: - coin.approve(self.swap, 2**256 - 1, sender=user) - - self.setup() - - def setup(self, user_id=0): - user = self.users[user_id] - for coin, q in zip(self.coins, self.initial_deposit): - mint_for_testing(coin, user, q) - - # Very first deposit - self.swap.add_liquidity(self.initial_deposit, 0, sender=user) - - self.balances = self.initial_deposit[:] - self.total_supply = self.swap.balanceOf(user) - self.xcp_profit = 10**18 - - def check_limits(self, amounts, D=True, y=True): - """ - Should be good if within limits, but if outside - can be either - """ - _D = self.swap.D() - prices = [10**18] + [self.swap.price_scale()] - xp_0 = [self.swap.balances(i) for i in range(2)] - xp = xp_0 - xp_0 = [x * p // 10**d for x, p, d in zip(xp_0, prices, self.decimals)] - xp = [(x + a) * p // 10**d for a, x, p, d in zip(amounts, xp, prices, self.decimals)] - - if D: - for _xp in [xp_0, xp]: - if ( - (min(_xp) * 10**18 // max(_xp) < 10**14) - or (max(_xp) < 10**9 * 10**18) - or (max(_xp) > 10**15 * 10**18) - ): - return False - - if y: - for _xp in [xp_0, xp]: - if ( - (_D < 10**17) - or (_D > 10**15 * 10**18) - or (min(_xp) * 10**18 // _D < 10**16) - or (max(_xp) * 10**18 // _D > 10**20) - ): - return False - - return True - - @rule( - exchange_amount_in=token_amount, - exchange_i=exchange_i, - user=user, - ) - def exchange(self, exchange_amount_in, exchange_i, user): - exchange_j = 1 - exchange_i - try: - calc_amount = self.swap.get_dy(exchange_i, exchange_j, exchange_amount_in) - except Exception: - _amounts = [0] * 2 - _amounts[exchange_i] = exchange_amount_in - if self.check_limits(_amounts) and exchange_amount_in > 10000: - raise - return None - - _amounts = [0] * 2 - _amounts[exchange_i] = exchange_amount_in - _amounts[exchange_j] = -calc_amount - limits_check = self.check_limits(_amounts) # If get_D fails - mint_for_testing(self.coins[exchange_i], user, exchange_amount_in) - - d_balance_i = self.coins[exchange_i].balanceOf(user) - d_balance_j = self.coins[exchange_j].balanceOf(user) - try: - self.coins[exchange_i].approve(self.swap, 2**256 - 1, sender=user) - out = self.swap.exchange(exchange_i, exchange_j, exchange_amount_in, 0, sender=user) - except Exception: - # Small amounts may fail with rounding errors - if ( - calc_amount > 100 - and exchange_amount_in > 100 - and calc_amount / self.swap.balances(exchange_j) > 1e-13 - and exchange_amount_in / self.swap.balances(exchange_i) > 1e-13 - and limits_check - ): - raise - return None - - # This is to check that we didn't end up in a borked state after - # an exchange succeeded - self.swap.get_dy( - exchange_j, - exchange_i, - 10**16 * 10 ** self.decimals[exchange_j] // INITIAL_PRICES[exchange_j], - ) - - d_balance_i -= self.coins[exchange_i].balanceOf(user) - d_balance_j -= self.coins[exchange_j].balanceOf(user) - - assert d_balance_i == exchange_amount_in - assert -d_balance_j == calc_amount, f"{-d_balance_j} vs {calc_amount}" - - self.balances[exchange_i] += d_balance_i - self.balances[exchange_j] += d_balance_j - - return out - - @rule(sleep_time=sleep_time) - def sleep(self, sleep_time): - boa.env.time_travel(sleep_time) - - @invariant() - def balances(self): - balances = [self.swap.balances(i) for i in range(2)] - balances_of = [c.balanceOf(self.swap) for c in self.coins] - for i in range(2): - assert self.balances[i] == balances[i] - assert self.balances[i] == balances_of[i] - - @invariant() - def total_supply(self): - assert self.total_supply == self.swap.totalSupply() - - @invariant() - def virtual_price(self): - virtual_price = self.swap.virtual_price() - xcp_profit = self.swap.xcp_profit() - get_virtual_price = self.swap.get_virtual_price() - - assert xcp_profit >= 10**18 - 10 - assert virtual_price >= 10**18 - 10 - assert get_virtual_price >= 10**18 - 10 - - assert xcp_profit - self.xcp_profit > -3, f"{xcp_profit} vs {self.xcp_profit}" - assert (virtual_price - 10**18) * 2 - ( - xcp_profit - 10**18 - ) >= -5, f"vprice={virtual_price}, xcp_profit={xcp_profit}" - assert abs(log(virtual_price / get_virtual_price)) < 1e-10 - - self.xcp_profit = xcp_profit - - @invariant() - def up_only_profit(self): - current_profit = xcp_profit = self.swap.xcp_profit() - xcp_profit_a = self.swap.xcp_profit_a() - current_profit = (xcp_profit + xcp_profit_a + 1) // 2 - - assert current_profit >= self.previous_pool_profit - self.previous_pool_profit = current_profit - - @contextlib.contextmanager - def upkeep_on_claim(self): - admin_balances_pre = [c.balanceOf(self.fee_receiver) for c in self.coins] - pool_is_ramping = self.swap.future_A_gamma_time() > boa.env.evm.state.patch.timestamp - - try: - yield - - finally: - new_xcp_profit_a = self.swap.xcp_profit_a() - old_xcp_profit_a = self.xcp_profit_a - - claimed = False - if new_xcp_profit_a > old_xcp_profit_a: - claimed = True - self.xcp_profit_a = new_xcp_profit_a - - admin_balances_post = [c.balanceOf(self.fee_receiver) for c in self.coins] - - if claimed: - for i in range(2): - claimed_amount = admin_balances_post[i] - admin_balances_pre[i] - assert claimed_amount > 0 # check if non zero amounts of claim - assert not pool_is_ramping # cannot claim while ramping - - # update self.balances - self.balances[i] -= claimed_amount - - self.xcp_profit = self.swap.xcp_profit() diff --git a/tests/unitary/pool/stateful/legacy/test_multiprecision.py b/tests/unitary/pool/stateful/legacy/test_multiprecision.py deleted file mode 100644 index d3500f2..0000000 --- a/tests/unitary/pool/stateful/legacy/test_multiprecision.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest -from boa.test import strategy -from hypothesis import HealthCheck, settings -from hypothesis.stateful import rule, run_state_machine_as_test - -from tests.unitary.pool.stateful.legacy.test_stateful import NumbaGoUp - -MAX_SAMPLES = 100 -STEP_COUNT = 100 - - -@pytest.fixture(scope="module") -def coins(stgusdc): - return stgusdc - - -@pytest.fixture(scope="module") -def swap(swap_multiprecision): - return swap_multiprecision - - -class Multiprecision(NumbaGoUp): - exchange_amount_in = strategy("uint256", min_value=10**18, max_value=50000 * 10**18) - user = strategy("address") - exchange_i = strategy("uint8", max_value=1) - - @rule( - exchange_amount_in=exchange_amount_in, - exchange_i=exchange_i, - user=user, - ) - def exchange(self, exchange_amount_in, exchange_i, user): - exchange_amount_in = exchange_amount_in // 10 ** (18 - self.decimals[exchange_i]) - super().exchange(exchange_amount_in, exchange_i, user) - - -def test_multiprecision(users, coins, swap): - Multiprecision.TestCase.settings = settings( - max_examples=MAX_SAMPLES, - stateful_step_count=STEP_COUNT, - suppress_health_check=list(HealthCheck), - deadline=None, - ) - - for k, v in locals().items(): - setattr(Multiprecision, k, v) - - # because of this hypothesis.event does not work - run_state_machine_as_test(Multiprecision) diff --git a/tests/unitary/pool/stateful/legacy/test_ramp.py b/tests/unitary/pool/stateful/legacy/test_ramp.py deleted file mode 100644 index 65c0b69..0000000 --- a/tests/unitary/pool/stateful/legacy/test_ramp.py +++ /dev/null @@ -1,126 +0,0 @@ -import boa -from hypothesis import HealthCheck, settings -from hypothesis import strategies as st -from hypothesis.stateful import ( - initialize, - invariant, - precondition, - rule, - run_state_machine_as_test, -) - -from tests.unitary.pool.stateful.legacy.test_stateful import NumbaGoUp -from tests.utils.constants import ( - MAX_A, - MAX_GAMMA, - MIN_A, - MIN_GAMMA, - MIN_RAMP_TIME, - UNIX_DAY, -) - -MAX_SAMPLES = 20 -STEP_COUNT = 100 - -# [0.2, 0.3 ... 0.9, 1, 2, 3 ... 10], used as sample values for the ramp step -change_steps = [x / 10 if x < 10 else x for x in range(2, 11)] + list(range(2, 11)) - - -class RampTest(NumbaGoUp): - """ - This class tests statefully tests wheter ramping A and - gamma does not break the pool. At the start it always start - with a ramp, then it can ramp again. - """ - - # we can only ramp A and gamma at most 10x - # lower/higher than their starting value - change_step_strategy = st.sampled_from(change_steps) - - # we fuzz the ramp duration up to a year - days = st.integers(min_value=1, max_value=365) - - def is_not_ramping(self): - """ - Checks if the pool is not already ramping. - """ - return boa.env.evm.patch.timestamp > self.swap.initial_A_gamma_time() + (MIN_RAMP_TIME - 1) - - @initialize( - A_change=change_step_strategy, - gamma_change=change_step_strategy, - days=days, - ) - def initial_ramp(self, A_change, gamma_change, days): - """ - At the start of the stateful test, we always ramp. - """ - self.__ramp(A_change, gamma_change, days) - - @precondition(is_not_ramping) - @rule( - A_change=change_step_strategy, - gamma_change=change_step_strategy, - days=days, - ) - def ramp(self, A_change, gamma_change, days): - """ - Additional ramping after the initial ramp. - Pools might ramp multiple times during their lifetime. - """ - self.__ramp(A_change, gamma_change, days) - - def __ramp(self, A_change, gamma_change, days): - """ - Computes the new A and gamma values by multiplying the current ones - by the change factors. Then clamps the new values to stay in the - [MIN_A, MAX_A] and [MIN_GAMMA, MAX_GAMMA] ranges. - - Then proceeds to ramp the pool with the new values (with admin rights). - """ - new_A = self.swap.A() * A_change - new_A = int(max(MIN_A, min(MAX_A, new_A))) # clamp new_A to stay in [MIN_A, MAX_A] - - new_gamma = self.swap.gamma() * gamma_change - new_gamma = int( - max(MIN_GAMMA, min(MAX_GAMMA, new_gamma)) - ) # clamp new_gamma to stay in [MIN_GAMMA, MAX_GAMMA] - - # current timestamp + fuzzed days - ramp_duration = boa.env.evm.patch.timestamp + days * UNIX_DAY - - self.swap.ramp_A_gamma( - new_A, - new_gamma, - ramp_duration, - sender=self.swap_admin, - ) - - @invariant() - def up_only_profit(self): - """ - We allow the profit to go down only because of the ramp. - """ - pass - - @invariant() - def virtual_price(self): - """ - We allow the profit to go down only because of the ramp. - """ - pass - - -def test_ramp(users, coins, swap): - RampTest.TestCase.settings = settings( - max_examples=MAX_SAMPLES, - stateful_step_count=STEP_COUNT, - suppress_health_check=list(HealthCheck), - deadline=None, - ) - - for k, v in locals().items(): - setattr(RampTest, k, v) - - # because of this hypothesis.event does not work - run_state_machine_as_test(RampTest) diff --git a/tests/unitary/pool/stateful/legacy/test_simulate.py b/tests/unitary/pool/stateful/legacy/test_simulate.py deleted file mode 100644 index d73402e..0000000 --- a/tests/unitary/pool/stateful/legacy/test_simulate.py +++ /dev/null @@ -1,105 +0,0 @@ -import boa -from boa.test import strategy -from hypothesis import HealthCheck, settings -from hypothesis.stateful import invariant, rule, run_state_machine_as_test - -from tests.unitary.pool.stateful.legacy.stateful_base import StatefulBase -from tests.utils import approx -from tests.utils import simulator as sim -from tests.utils.tokens import mint_for_testing - -MAX_SAMPLES = 20 -STEP_COUNT = 100 - - -class StatefulSimulation(StatefulBase): - exchange_amount_in = strategy("uint256", min_value=10**17, max_value=10**5 * 10**18) - exchange_i = strategy("uint8", max_value=1) - user = strategy("address") - - def setup(self): - super().setup() - - for u in self.users[1:]: - for coin, q in zip(self.coins, self.initial_deposit): - mint_for_testing(coin, u, q) - for i in range(2): - self.balances[i] += self.initial_deposit[i] - self.swap.add_liquidity(self.initial_deposit, 0, sender=u) - self.total_supply += self.swap.balanceOf(u) - - self.virtual_price = self.swap.get_virtual_price() - - self.trader = sim.Trader( - self.swap.A(), - self.swap.gamma(), - self.swap.D(), - [10**18, self.swap.price_scale()], - self.swap.mid_fee() / 1e10, - self.swap.out_fee() / 1e10, - self.swap.fee_gamma(), - self.swap.adjustment_step() / 1e18, - int(self.swap.ma_time() / 0.693), # crypto swap returns ma time in sec - ) - for i in range(2): - self.trader.curve.x[i] = self.swap.balances(i) - - # Adjust virtual prices - self.trader.xcp_profit = self.swap.xcp_profit() - self.trader.xcp_profit_real = self.swap.virtual_price() - self.trader.t = boa.env.evm.patch.timestamp - self.swap_no = 0 - - @rule( - exchange_amount_in=exchange_amount_in, - exchange_i=exchange_i, - user=user, - ) - def exchange(self, exchange_amount_in, exchange_i, user): - dx = exchange_amount_in * 10**18 // self.trader.price_oracle[exchange_i] - self.swap_no += 1 - super().exchange(dx, exchange_i, user) - - if not self.swap_out: - return # if swap breaks, dont check. - - dy_trader = self.trader.buy(dx, exchange_i, 1 - exchange_i) - self.trader.tweak_price(boa.env.evm.patch.timestamp) - - # exchange checks: - assert approx(self.swap_out, dy_trader, 1e-3) - assert approx(self.swap.price_oracle(), self.trader.price_oracle[1], 1.5e-3) - - boa.env.time_travel(12) - - @invariant() - def simulator(self): - if self.trader.xcp_profit / 1e18 - 1 > 1e-8: - assert ( - abs(self.trader.xcp_profit - self.swap.xcp_profit()) - / (self.trader.xcp_profit - 10**18) - < 0.05 - ) - - price_scale = self.swap.price_scale() - price_trader = self.trader.curve.p[1] - try: - assert approx(price_scale, price_trader, 1e-3) - except Exception: - if self.check_limits([0, 0, 0]): - assert False - - -def test_sim(users, coins, swap): - StatefulSimulation.TestCase.settings = settings( - max_examples=MAX_SAMPLES, - stateful_step_count=STEP_COUNT, - suppress_health_check=list(HealthCheck), - deadline=None, - ) - - for k, v in locals().items(): - setattr(StatefulSimulation, k, v) - - # because of this hypothesis.event does not work - run_state_machine_as_test(StatefulSimulation) diff --git a/tests/unitary/pool/stateful/legacy/test_stateful.py b/tests/unitary/pool/stateful/legacy/test_stateful.py deleted file mode 100644 index 091fe7c..0000000 --- a/tests/unitary/pool/stateful/legacy/test_stateful.py +++ /dev/null @@ -1,167 +0,0 @@ -import boa -from hypothesis import HealthCheck, settings -from hypothesis.stateful import ( - Bundle, - precondition, - rule, - run_state_machine_as_test, -) - -from tests.fixtures.pool import INITIAL_PRICES -from tests.unitary.pool.stateful.legacy.stateful_base import StatefulBase -from tests.utils.tokens import mint_for_testing - -MAX_SAMPLES = 20 -STEP_COUNT = 100 -MAX_D = 10**12 * 10**18 # $1T is hopefully a reasonable cap for tests - - -class NumbaGoUp(StatefulBase): - """ - Test that profit goes up - """ - - depositor = Bundle("depositor") - - def supply_not_too_big(self): - # this is not stableswap so hard - # to say what is a good limit - return self.swap.D() < MAX_D - - def pool_not_empty(self): - return self.total_supply != 0 - - @precondition(supply_not_too_big) - @rule( - target=depositor, - deposit_amounts=StatefulBase.two_token_amounts, - user=StatefulBase.user, - ) - def add_liquidity(self, amounts, user): - if sum(amounts) == 0: - return str(user) - - new_balances = [x + y for x, y in zip(self.balances, amounts)] - - for coin, q in zip(self.coins, amounts): - mint_for_testing(coin, user, q) - - try: - tokens = self.swap.balanceOf(user) - self.swap.add_liquidity(amounts, 0, sender=user) - tokens = self.swap.balanceOf(user) - tokens - self.total_supply += tokens - self.balances = new_balances - - except Exception: - if self.check_limits(amounts): - raise - return str(user) - - # This is to check that we didn't end up in a borked state after - # an exchange succeeded - try: - self.swap.get_dy(0, 1, 10 ** (self.decimals[0] - 2)) - except Exception: - self.swap.get_dy( - 1, - 0, - 10**16 * 10 ** self.decimals[1] // self.swap.price_scale(), - ) - return str(user) - - @precondition(pool_not_empty) - @rule(token_amount=StatefulBase.token_amount, user=depositor) - def remove_liquidity(self, token_amount, user): - if self.swap.balanceOf(user) < token_amount or token_amount == 0: - print("Skipping") - # no need to have this case in stateful - with boa.reverts(): - self.swap.remove_liquidity(token_amount, [0] * 2, sender=user) - else: - print("Removing") - amounts = [c.balanceOf(user) for c in self.coins] - tokens = self.swap.balanceOf(user) - with self.upkeep_on_claim(): - self.swap.remove_liquidity(token_amount, [0] * 2, sender=user) - tokens -= self.swap.balanceOf(user) - self.total_supply -= tokens - amounts = [(c.balanceOf(user) - a) for c, a in zip(self.coins, amounts)] - self.balances = [b - a for a, b in zip(amounts, self.balances)] - - # Virtual price resets if everything is withdrawn - if self.total_supply == 0: - self.virtual_price = 10**18 - - @precondition(pool_not_empty) - @rule( - token_amount=StatefulBase.token_amount, - exchange_i=StatefulBase.exchange_i, - user=depositor, - ) - def remove_liquidity_one_coin(self, token_amount, exchange_i, user): - try: - calc_out_amount = self.swap.calc_withdraw_one_coin(token_amount, exchange_i) - except Exception: - if ( - self.check_limits([0] * 2) - and not (token_amount > self.total_supply) - and token_amount > 10000 - ): - self.swap.calc_withdraw_one_coin(token_amount, exchange_i, sender=user) - return - - d_token = self.swap.balanceOf(user) - if d_token < token_amount: - with boa.reverts(): - self.swap.remove_liquidity_one_coin(token_amount, exchange_i, 0, sender=user) - return - - d_balance = self.coins[exchange_i].balanceOf(user) - try: - with self.upkeep_on_claim(): - self.swap.remove_liquidity_one_coin(token_amount, exchange_i, 0, sender=user) - except Exception: - # Small amounts may fail with rounding errors - if ( - calc_out_amount > 100 - and token_amount / self.total_supply > 1e-10 - and calc_out_amount / self.swap.balances(exchange_i) > 1e-10 - ): - raise - return - - # This is to check that we didn't end up in a borked state after - # an exchange succeeded - _deposit = [0] * 2 - _deposit[exchange_i] = ( - 10**16 * 10 ** self.decimals[exchange_i] // ([10**18] + INITIAL_PRICES)[exchange_i] - ) - assert self.swap.calc_token_amount(_deposit, True) - - d_balance = self.coins[exchange_i].balanceOf(user) - d_balance - d_token = d_token - self.swap.balanceOf(user) - - assert calc_out_amount == d_balance, f"{calc_out_amount} vs {d_balance} for {token_amount}" - - self.balances[exchange_i] -= d_balance - self.total_supply -= d_token - - # Virtual price resets if everything is withdrawn - if self.total_supply == 0: - self.virtual_price = 10**18 - - -def test_numba_go_up(users, coins, swap): - NumbaGoUp.TestCase.settings = settings( - max_examples=MAX_SAMPLES, - stateful_step_count=STEP_COUNT, - suppress_health_check=list(HealthCheck), - deadline=None, - ) - - for k, v in locals().items(): - setattr(NumbaGoUp, k, v) - - # because of this hypothesis.event does not work - run_state_machine_as_test(NumbaGoUp)