Skip to content

Commit

Permalink
feat: more features in new stateful test
Browse files Browse the repository at this point in the history
* 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`
  • Loading branch information
AlbertoCentonze committed May 8, 2024
1 parent 866e545 commit ca40664
Showing 1 changed file with 74 additions and 13 deletions.
87 changes: 74 additions & 13 deletions tests/unitary/pool/stateful/stateful_base2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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):

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand Down

0 comments on commit ca40664

Please sign in to comment.