From ca4066476b8b7a0305e8ecdf0726d21923c9ba64 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 8 May 2024 15:29:25 +0200 Subject: [PATCH] feat: more features in new stateful test * the amount of liquidity with which pools are seeded is now bigger, since it doesn't make sense to test pools with shallow liquidity (newton_y math breaks). * added two new invariants `can_always_withdraw` and `newton_y_converges` that make sure the maths of the pool are not broken. The former attemps to withdraw all available liquidity (this verifies newton_D always works), the latter attemps to call get_y with an amount that could fail when the pool is severely unbalanced. * the `add_liquidity` function now adds the depositor to a list and sanity checks make sure we don't store the address of depositor who actually does not have a deposit (useful for withdrawals in the future). * `exchange` method now works and performs assertion to make sure swaps are correct. * up_only_profit now also updates the state machine about `xcp_profit` and `xcp_profit_a` --- tests/unitary/pool/stateful/stateful_base2.py | 87 ++++++++++++++++--- 1 file changed, 74 insertions(+), 13 deletions(-) diff --git a/tests/unitary/pool/stateful/stateful_base2.py b/tests/unitary/pool/stateful/stateful_base2.py index eedabcc2..3e5400cb 100644 --- a/tests/unitary/pool/stateful/stateful_base2.py +++ b/tests/unitary/pool/stateful/stateful_base2.py @@ -16,19 +16,20 @@ class StatefulBase(RuleBasedStateMachine): - pool = None - user_balances = dict() - total_supply = 0 - # try bigger amounts than 30 and e11 for low @initialize( pool=pool_strategy(), - amount=integers(min_value=int(1e11), max_value=int(1e30)), + # TODO deposits can be as low as 1e11, but small deposits breaks swaps + # I should do stateful testing only with deposit withdrawal + amount=integers(min_value=int(1e20), max_value=int(1e30)), ) def initialize_pool(self, pool, amount): # cahing the pool generated by the strategy self.pool = pool + # total supply of lp tokens (updated from reported balances) + self.total_supply = 0 + # caching coins here for easier access self.coins = [ERC20.at(pool.coins(i)) for i in range(2)] @@ -40,6 +41,8 @@ def initialize_pool(self, pool, amount): self.xcp_profit_a = 1e18 self.xcpx = 1e18 + self.depositors = [] + # deposit some initial liquidity balanced_amounts = self.get_balanced_deposit_amounts(amount) note( @@ -106,7 +109,7 @@ def add_liquidity(self, amounts: List[int], user: str) -> str: self.xcp_profit = self.pool.xcp_profit() self.xcp_profit_a = self.pool.xcp_profit_a() - return user + self.depositors.append(user) def exchange(self, dx: int, i: int, user: str): """Wrapper around the `exchange` method of the pool. @@ -120,15 +123,32 @@ def exchange(self, dx: int, i: int, user: str): # j is the index of the coin that comes out of the pool j = 1 - i - # TODO if this fails... handle it + mint_for_testing(self.coins[i], user, dx) + self.coins[i].approve(self.pool.address, dx, sender=user) + + delta_balance_i = self.coins[i].balanceOf(user) + delta_balance_j = self.coins[j].balanceOf(user) + expected_dy = self.pool.get_dy(i, j, dx) - mint_for_testing(self.coins[i], user, dx) - self.coins[i].approve(self.pool(dx, sender=user)) actual_dy = self.pool.exchange(i, j, dx, expected_dy, sender=user) - assert ( - actual_dy == expected_dy - ) # TODO == self.coins[j].balanceOf(user) + + delta_balance_i = self.coins[i].balanceOf(user) - delta_balance_i + delta_balance_j = self.coins[j].balanceOf(user) - delta_balance_j + + assert -delta_balance_i == dx + assert delta_balance_j == expected_dy == actual_dy + + self.balances[i] -= delta_balance_i + self.balances[j] -= delta_balance_j + + self.xcp_profit = self.pool.xcp_profit() + + note( + "exchanged {:.2e} of token {} for {:.2e} of token {}".format( + dx, i, actual_dy, j + ) + ) def remove_liquidity(self, amount, user): @@ -150,6 +170,42 @@ def time_forward(self, time_increase): # --------------- pool invariants ---------------------- + @invariant() + def newton_y_converges(self): + """We use get_dy with a small amount to check if the newton_y + still manages to find the correct value. If this is not the case + the pool is broken and it can't execute swaps anymore. + """ + ARBITRARY_SMALL_AMOUNT = int(1e15) + try: + self.pool.get_dy(0, 1, ARBITRARY_SMALL_AMOUNT) + try: + self.pool.get_dy(1, 0, ARBITRARY_SMALL_AMOUNT) + except Exception: + raise AssertionError("newton_y is broken") + except Exception: + pass + + @invariant() + def can_always_withdraw(self): + """Make sure that newton_D always works when withdrawing liquidity. + No matter how imbalanced the pool is, it should always be possible + to withdraw liquidity in a proportional way. + """ + + # TODO we need to check that: + # - y is not broken (through get_dy) + with boa.env.anchor(): + for d in self.depositors: + prev_balances = [c.balanceOf(self.pool) for c in self.coins] + tokens = self.pool.balanceOf(d) + self.pool.remove_liquidity(tokens, [0] * 2, sender=d) + # assert current balances are less as the previous ones + for c, b in zip(self.coins, prev_balances): + assert c.balanceOf(self.pool) < b + for c in self.coins: + assert c.balanceOf(self.pool) == 0 + @invariant() def sleep(self): pass # TODO @@ -173,6 +229,9 @@ def sanity_check(self): assert self.pool.virtual_price() >= 1e18 assert self.pool.get_virtual_price() >= 1e18 + for d in self.depositors: + assert self.pool.balanceOf(d) > 0 + @invariant() def virtual_price(self): pass # TODO @@ -188,7 +247,7 @@ def up_only_profit(self): You can imagine `xcpx` as a value that that is always between the interval [xcp_profit, xcp_profit_a]. When `xcp` goes down - when claiming fees, `xcp_a` goes up. Averagin them creates this + when claiming fees, `xcp_a` goes up. Averaging them creates this measure of profit that only goes down when something went wrong. """ xcp_profit = self.pool.xcp_profit() @@ -199,6 +258,8 @@ def up_only_profit(self): assert xcpx >= self.xcpx # updates the previous profit self.xcpx = xcpx + self.xcp_profit = xcp_profit + self.xcp_profit_a = xcp_profit_a TestBase = StatefulBase.TestCase