Skip to content

Commit

Permalink
feat: added new stateful testing class (wip)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertoCentonze committed May 7, 2024
1 parent b61d99b commit 866e545
Showing 1 changed file with 208 additions and 0 deletions.
208 changes: 208 additions & 0 deletions tests/unitary/pool/stateful/stateful_base2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
from typing import List

import boa
from hypothesis import event, note
from hypothesis.stateful import (
RuleBasedStateMachine,
initialize,
invariant,
rule,
)
from hypothesis.strategies import integers
from strategies import pool as pool_strategy

from contracts.mocks import ERC20Mock as ERC20
from tests.utils.tokens import mint_for_testing


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)),
)
def initialize_pool(self, pool, amount):
# cahing the pool generated by the strategy
self.pool = pool

# caching coins here for easier access
self.coins = [ERC20.at(pool.coins(i)) for i in range(2)]

# these balances should follow the pool balances
self.balances = [0, 0]

# initial profit is 1e18
self.xcp_profit = 1e18
self.xcp_profit_a = 1e18
self.xcpx = 1e18

# deposit some initial liquidity
balanced_amounts = self.get_balanced_deposit_amounts(amount)
note(
"seeding pool with balanced amounts: {:.2e} {:.2e}".format(
*balanced_amounts
)
)
self.add_liquidity(balanced_amounts, boa.env.generate_address())

def get_balanced_deposit_amounts(self, amount: int):
"""Get the amounts of tokens that should be deposited
to the pool to have balanced amounts of the two tokens.
Args:
amount (int): the amount of the first token
Returns:
List[int]: the amounts of the two tokens
"""
return [int(amount), int(amount * 1e18 // self.pool.price_scale())]

# --------------- pool methods ---------------
# methods that wrap the pool methods that should be used in
# the rules of the state machine. These methods make sure that
# both the state of the pool and of the state machine are
# updated together. Calling pool methods directly will probably
# lead to incorrect simulation and errors.

def add_liquidity(self, amounts: List[int], user: str) -> str:
"""Wrapper around the `add_liquidity` method of the pool.
Always prefer this instead of calling the pool method directly.
Args:
amounts (List[int]): amounts of tokens to be deposited
user (str): the sender of the transaction
Returns:
str: the address of the depositor
"""
if sum(amounts) == 0:
event("empty deposit")
return

for coin, amount in zip(self.coins, amounts):
coin.approve(self.pool, 2**256 - 1, sender=user)
mint_for_testing(coin, user, amount)

# store the amount of lp tokens before the deposit
lp_tokens = self.pool.balanceOf(user)

# TODO stricter since no slippage
self.pool.add_liquidity(amounts, 0, sender=user)

# find the increase in lp tokens
lp_tokens = self.pool.balanceOf(user) - lp_tokens
# increase the total supply by the amount of lp tokens
self.total_supply += lp_tokens

# pool balances should increase by the amounts
self.balances = [x + y for x, y in zip(self.balances, amounts)]

# update the profit since it increases through `tweak_price`
# which is called by `add_liquidity`
self.xcp_profit = self.pool.xcp_profit()
self.xcp_profit_a = self.pool.xcp_profit_a()

return user

def exchange(self, dx: int, i: int, user: str):
"""Wrapper around the `exchange` method of the pool.
Always prefer this instead of calling the pool method directly.
Args:
dx (int): amount in
i (int): the token the user sends to swap
user (str): the sender of the transaction
"""
# j is the index of the coin that comes out of the pool
j = 1 - i

# TODO if this fails... handle it
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)

def remove_liquidity(self, amount, user):

# virtual price resets if everything is withdrawn
if self.total_supply == 0:
event("full liquidity removal")
self.virtual_price = 1e18

def remove_liquidity_one_coin(self, percentage):
pass

@rule(time_increase=integers(min_value=1, max_value=86400 * 7))
def time_forward(self, time_increase):
"""Make the time moves forward by `sleep_time` seconds.
Useful for ramping, oracle updates, etc.
Up to 1 week.
"""
boa.env.time_travel(time_increase)

# --------------- pool invariants ----------------------

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

@invariant()
def balances(self):
balances = [self.pool.balances(i) for i in range(2)]
balances_of = [c.balanceOf(self.pool) 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 sanity_check(self):
"""Make sure the stateful simulations matches the contract state."""
assert self.xcp_profit == self.pool.xcp_profit()
assert self.total_supply == self.pool.totalSupply()

# profit, cached vp and current vp should be at least 1e18
assert self.xcp_profit >= 1e18
assert self.pool.virtual_price() >= 1e18
assert self.pool.get_virtual_price() >= 1e18

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

@invariant()
def up_only_profit(self):
"""This method checks if the pool is profitable, since it should
never lose money.
To do so we use the so called `xcpx`. This is an emprical measure
of profit that is even stronger than `xcp`. We have to use this
because `xcp` goes down when claiming admin fees.
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
measure of profit that only goes down when something went wrong.
"""
xcp_profit = self.pool.xcp_profit()
xcp_profit_a = self.pool.xcp_profit_a()
xcpx = (xcp_profit + xcp_profit_a + 1e18) // 2

# make sure that the previous profit is smaller than the current
assert xcpx >= self.xcpx
# updates the previous profit
self.xcpx = xcpx


TestBase = StatefulBase.TestCase

# TODO make sure that xcp goes down when claiming admin fees
# TODO add an invariant with withdrawal simulations to make sure
# it is always possible

0 comments on commit 866e545

Please sign in to comment.