Skip to content

Commit

Permalink
feat: TestUpOnlyLiquidity stateful testing
Browse files Browse the repository at this point in the history
* implemented new stateful test that also continuously deposits balanced liquidity in the pool (this didn't happen in the previous version of stateful tests)

* addresses are now generated using boa's search strategy instead of using `boa.env.generate_address()` this will be greatly beneficial when vyperlang/titanoboa#208 will be implemented.

* search strategy to generate factories now makes sure the addresses of `fee_receiver`, `owner` and `deployer` are different.

* `exchange_rule` now can only swap up to 60% of the pool liqudity since 85% would often break `newton_y` (and it seems like a reasonably unlikely to happen situtation).

* `StatefulBase` now tracks depositors with a set instead of a list to avoid having duplicates addresses when someone deposits multiple times.

* removed `sleep` invariant since this is already implemented as the `time_forward` rule (using `sleep` as name was leading to unexpected behavior because of python module with the same name).

* more comments on what has been done so far.
  • Loading branch information
AlbertoCentonze committed May 9, 2024
1 parent 978590e commit b24b71c
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 35 deletions.
39 changes: 28 additions & 11 deletions tests/unitary/pool/stateful/stateful_base2.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
rule,
)
from hypothesis.strategies import integers
from strategies import address
from strategies import pool as pool_strategy

from contracts.mocks import ERC20Mock as ERC20
Expand All @@ -23,8 +24,9 @@ class StatefulBase(RuleBasedStateMachine):
# 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)),
user=address,
)
def initialize_pool(self, pool, amount):
def initialize_pool(self, pool, amount, user):
# cahing the pool generated by the strategy
self.pool = pool

Expand All @@ -42,7 +44,7 @@ def initialize_pool(self, pool, amount):
self.xcp_profit_a = 1e18
self.xcpx = 1e18

self.depositors = []
self.depositors = set()

# deposit some initial liquidity
balanced_amounts = self.get_balanced_deposit_amounts(amount)
Expand All @@ -51,7 +53,9 @@ def initialize_pool(self, pool, amount):
*balanced_amounts
)
)
self.add_liquidity(balanced_amounts, boa.env.generate_address())
self.add_liquidity(balanced_amounts, user)

# --------------- utility methods ---------------

def get_balanced_deposit_amounts(self, amount: int):
"""Get the amounts of tokens that should be deposited
Expand All @@ -72,7 +76,7 @@ def get_balanced_deposit_amounts(self, amount: int):
# updated together. Calling pool methods directly will probably
# lead to incorrect simulation and errors.

def add_liquidity(self, amounts: List[int], user: str) -> str:
def add_liquidity(self, amounts: List[int], user: str):
"""Wrapper around the `add_liquidity` method of the pool.
Always prefer this instead of calling the pool method directly.
Expand All @@ -83,12 +87,15 @@ def add_liquidity(self, amounts: List[int], user: str) -> str:
Returns:
str: the address of the depositor
"""
# check to prevent revert on empty deposits
if sum(amounts) == 0:
event("empty deposit")
return

for coin, amount in zip(self.coins, amounts):
# infinite approval
coin.approve(self.pool, 2**256 - 1, sender=user)
# mint the amount of tokens for the depositor
mint_for_testing(coin, user, amount)

# store the amount of lp tokens before the deposit
Expand All @@ -110,7 +117,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()

self.depositors.append(user)
self.depositors.add(user)

def exchange(self, dx: int, i: int, user: str):
"""Wrapper around the `exchange` method of the pool.
Expand Down Expand Up @@ -152,6 +159,15 @@ def exchange(self, dx: int, i: int, user: str):
)

def remove_liquidity(self, amount, user):
amounts = [c.balanceOf(user) for c in self.coins]
tokens = self.swap.balanceOf(user)
self.pool.remove_liquidity(amount, [0] * 2, sender=user)
amounts = [
(c.balanceOf(user) - a) for c, a in zip(self.coins, amounts)
]
self.total_supply -= tokens
tokens -= self.swap.balanceOf(user)
self.balances = [b - a for a, b in zip(amounts, self.balances)]

# virtual price resets if everything is withdrawn
if self.total_supply == 0:
Expand Down Expand Up @@ -194,23 +210,24 @@ def can_always_withdraw(self):
to withdraw liquidity in a proportional way.
"""

# TODO we need to check that:
# - y is not broken (through get_dy)
# anchor the environment to make sure that the balances are
# restored after the invariant is checked
with boa.env.anchor():
# remove all liquidity from all depositors
for d in self.depositors:
# store the current balances of the pool
prev_balances = [c.balanceOf(self.pool) for c in self.coins]
# withdraw all liquidity from the depositor
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):
# check that the balance of the pool is less than before
assert c.balanceOf(self.pool) < b
for c in self.coins:
# there should not be any liquidity left in the pool
assert c.balanceOf(self.pool) == 0

@invariant()
def sleep(self):
pass # TODO

@invariant()
def balances(self):
balances = [self.pool.balances(i) for i in range(2)]
Expand Down
17 changes: 11 additions & 6 deletions tests/unitary/pool/stateful/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"""

import boa
from hypothesis import note
from boa.test import strategy
from hypothesis import assume, note
from hypothesis.strategies import (
composite,
integers,
Expand All @@ -29,12 +30,14 @@
MIN_GAMMA,
)

# just a more hyptohesis-like way to get an address
# from boa's search strategy
address = strategy("address")

# ---------------- addresses ----------------
# TODO this should use the boa address strategy
# when the recurring address feature gets added
deployer = just(boa.env.generate_address())
fee_receiver = just(boa.env.generate_address())
owner = just(boa.env.generate_address())
deployer = address
fee_receiver = address
owner = address


# ---------------- factory ----------------
Expand All @@ -46,6 +49,8 @@ def factory(
_fee_receiver = draw(fee_receiver)
_owner = draw(owner)

assume(_fee_receiver != _owner != _deployer)

with boa.env.prank(_deployer):
amm_implementation = amm_deployer.deploy_as_blueprint()
gauge_implementation = gauge_deployer.deploy_as_blueprint()
Expand Down
55 changes: 37 additions & 18 deletions tests/unitary/pool/stateful/test_stateful2.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import boa
from hypothesis import note
from hypothesis.stateful import rule
from hypothesis.strategies import data, integers, just
from hypothesis import note, settings
from hypothesis.stateful import precondition, rule
from hypothesis.strategies import data, integers
from stateful_base2 import StatefulBase
from strategies import address

settings.register_profile("no_shrinking", settings(phases=[]))


class OnlySwapStateful(StatefulBase):
Expand All @@ -14,31 +16,48 @@ class OnlySwapStateful(StatefulBase):
@rule(
data=data(),
i=integers(min_value=0, max_value=1),
user=just(boa.env.generate_address()),
user=address,
)
def exchange(self, data, i: int, user: str):
def exchange_rule(self, data, i: int, user: str):
liquidity = self.coins[i].balanceOf(self.pool.address)
# we use a data strategy since the amount we want to swap
# depends on the pool liquidity which is only known at runtime
dx = data.draw(
integers(
# swap can be between 0.001% and 60% of the pool liquidity
min_value=int(liquidity * 0.0001),
max_value=int(liquidity * 0.85),
max_value=int(liquidity * 0.60),
),
label="dx",
)
note("trying to swap: {:.3%} of pool liquidity".format(dx / liquidity))
return super().exchange(dx, i, user)
self.exchange(dx, i, user)


class UpOnlyLiquidityStateful(OnlySwapStateful):
"""This test suite does everything as the OnlySwapStateful
"""This test suite does everything as the `OnlySwapStateful`
but also adds liquidity to the pool. It does not remove liquidity."""

# TODO
pass
# too high liquidity can lead to overflows
@precondition(lambda self: self.pool.D() < 1e28)
@rule(
# we can only add liquidity up to 1e25, this was reduced
# from the initial deposit that can be up to 1e30 to avoid
# breaking newton_D
amount=integers(min_value=int(1e20), max_value=int(1e25)),
user=address,
)
def add_liquidity_balanced_rule(self, amount: int, user: str):
balanced_amounts = self.get_balanced_deposit_amounts(amount)
note(
"increasing pool liquidity with balanced amounts: "
+ "{:.2e} {:.2e}".format(*balanced_amounts)
)
self.add_liquidity(balanced_amounts, user)


class OnlyBalancedLiquidityStateful(UpOnlyLiquidityStateful):
"""This test suite does everything as the UpOnlyLiquidityStateful
"""This test suite does everything as the `UpOnlyLiquidityStateful`
but also removes liquidity from the pool. Both deposits and withdrawals
are balanced.
"""
Expand All @@ -48,9 +67,8 @@ class OnlyBalancedLiquidityStateful(UpOnlyLiquidityStateful):


class UnbalancedLiquidityStateful(OnlyBalancedLiquidityStateful):
"""This test suite does everything as the OnlyBalancedLiquidityStateful
but also removes liquidity from the pool. Deposits and withdrawals can
be unbalanced.
"""This test suite does everything as the `OnlyBalancedLiquidityStateful`
Deposits and withdrawals can be unbalanced.
This is the most complex test suite and should be used when making sure
that some specific gamma and A can be used without unexpected behavior.
Expand All @@ -61,7 +79,7 @@ class UnbalancedLiquidityStateful(OnlyBalancedLiquidityStateful):


class RampingStateful(UnbalancedLiquidityStateful):
"""This test suite does everything as the UnbalancedLiquidityStateful
"""This test suite does everything as the `UnbalancedLiquidityStateful`
but also ramps the pool. Because of this some of the invariant checks
are disabled (loss is expected).
"""
Expand All @@ -70,8 +88,9 @@ class RampingStateful(UnbalancedLiquidityStateful):
pass


TestOnlySwap = OnlySwapStateful.TestCase
# TestUpOnlyLiquidity = UpOnlyLiquidityStateful.TestCase
# TestOnlySwap = OnlySwapStateful.TestCase
TestUpOnlyLiquidity = UpOnlyLiquidityStateful.TestCase
# TestOnlyBalancedLiquidity = OnlyBalancedLiquidityStateful.TestCase
# TestUnbalancedLiquidity = UnbalancedLiquidityStateful.TestCase
# RampingStateful = RampingStateful.TestCase
# TODO variable decimals

0 comments on commit b24b71c

Please sign in to comment.