Skip to content

Commit 97a644b

Browse files
committed
begin stateful tests
1 parent aea8d70 commit 97a644b

File tree

4 files changed

+216
-1
lines changed

4 files changed

+216
-1
lines changed

contracts/mocks/ERC20Mock.vy

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,12 @@ def approve(_spender: address, _value: uint256) -> bool:
6060
self.allowances[msg.sender][_spender] = _value
6161
log Approval(msg.sender, _spender, _value)
6262
return True
63+
64+
65+
@external
66+
def _mint_for_testing(_target: address, _value: uint256) -> bool:
67+
self.totalSupply += _value
68+
self.balanceOf[_target] += _value
69+
log Transfer(ZERO_ADDRESS, _target, _value)
70+
71+
return True

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ pdbpp
1919
hypothesis>=6.68.1
2020

2121
# vyper and dev framework:
22-
git+https://github.com/vyperlang/titanoboa@8485787c5196718c679f82e7ac69fef585a4ce1d
22+
git+https://github.com/vyperlang/titanoboa@b5e9fb96d1424ed5cc5a6af03391d885439c83e5
2323
vyper>=0.3.10
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
from math import log
2+
3+
from boa.test import strategy
4+
5+
MAX_SAMPLES = 20
6+
MAX_D = 10 ** 12 * 10 ** 18 # $1T is hopefully a reasonable cap for tests
7+
INITIAL_PRICES = [10**18, 1500 * 10**18] # price relative to coin_id = 0
8+
9+
10+
class StatefulBase:
11+
exchange_amount_in = strategy("uint256", max_value=10 ** 9 * 10 ** 18)
12+
exchange_i = strategy("uint8", max_value=1)
13+
sleep_time = strategy("uint256", max_value=86400 * 7)
14+
user = strategy("address")
15+
16+
def __init__(self, accounts, coins, crypto_swap, token):
17+
self.accounts = accounts
18+
self.swap = crypto_swap
19+
self.coins = coins
20+
self.token = token
21+
22+
def setup(self, user_id=0):
23+
self.decimals = [int(c.decimals()) for c in self.coins]
24+
self.user_balances = {u: [0] * 2 for u in self.accounts}
25+
self.initial_deposit = [
26+
10 ** 4 * 10 ** (18 + d) // p
27+
for p, d in zip([10 ** 18] + INITIAL_PRICES, self.decimals)
28+
] # $10k * 2
29+
self.initial_prices = [10 ** 18] + INITIAL_PRICES
30+
user = self.accounts[user_id]
31+
32+
for coin, q in zip(self.coins, self.initial_deposit):
33+
coin._mint_for_testing(user, q)
34+
coin.approve(self.swap, 2 ** 256 - 1, {"from": user})
35+
36+
# Inf approve all, too. Not always that's the best way though
37+
for u in self.accounts:
38+
if u != user:
39+
for coin in self.coins:
40+
coin.approve(self.swap, 2 ** 256 - 1, {"from": u})
41+
42+
# Very first deposit
43+
self.swap.add_liquidity(self.initial_deposit, 0, {"from": user})
44+
45+
self.balances = self.initial_deposit[:]
46+
self.total_supply = self.token.balanceOf(user)
47+
self.xcp_profit = 10 ** 18
48+
print(" \n ----------- INIT ----------------- ")
49+
50+
def convert_amounts(self, amounts):
51+
prices = [10 ** 18] + [self.swap.price_scale()]
52+
return [p * a // 10 ** (36 - d) for p, a, d in zip(prices, amounts, self.decimals)]
53+
54+
def check_limits(self, amounts, D=True, y=True):
55+
"""
56+
Should be good if within limits, but if outside - can be either
57+
"""
58+
_D = self.swap.D()
59+
prices = [10 ** 18] + [self.swap.price_scale()]
60+
xp_0 = [self.swap.balances(i) for i in range(2)]
61+
xp = xp_0
62+
xp_0 = [x * p // 10 ** d for x, p, d in zip(xp_0, prices, self.decimals)]
63+
xp = [(x + a) * p // 10 ** d for a, x, p, d in zip(amounts, xp, prices, self.decimals)]
64+
65+
if D:
66+
for _xp in [xp_0, xp]:
67+
if (
68+
(min(_xp) * 10 ** 18 // max(_xp) < 10 ** 14)
69+
or (max(_xp) < 10 ** 9 * 10 ** 18)
70+
or (max(_xp) > 10 ** 15 * 10 ** 18)
71+
):
72+
return False
73+
74+
if y:
75+
for _xp in [xp_0, xp]:
76+
if (
77+
(_D < 10 ** 17)
78+
or (_D > 10 ** 15 * 10 ** 18)
79+
or (min(_xp) * 10 ** 18 // _D < 10 ** 16)
80+
or (max(_xp) * 10 ** 18 // _D > 10 ** 20)
81+
):
82+
return False
83+
84+
return True
85+
86+
def rule_exchange(self, exchange_amount_in, exchange_i, user):
87+
return self._rule_exchange(exchange_amount_in, exchange_i, user)
88+
89+
def _rule_exchange(self, exchange_amount_in, exchange_i, user, check_out_amount=True):
90+
exchange_j = 1 - exchange_i
91+
try:
92+
calc_amount = self.swap.get_dy(exchange_i, exchange_j, exchange_amount_in)
93+
except Exception:
94+
_amounts = [0] * 2
95+
_amounts[exchange_i] = exchange_amount_in
96+
if self.check_limits(_amounts) and exchange_amount_in > 10000:
97+
raise
98+
return False
99+
self.coins[exchange_i]._mint_for_testing(user, exchange_amount_in)
100+
101+
d_balance_i = self.coins[exchange_i].balanceOf(user)
102+
d_balance_j = self.coins[exchange_j].balanceOf(user)
103+
try:
104+
self.swap.exchange(exchange_i, exchange_j, exchange_amount_in, 0, {"from": user})
105+
except Exception:
106+
# Small amounts may fail with rounding errors
107+
if (
108+
calc_amount > 100
109+
and exchange_amount_in > 100
110+
and calc_amount / self.swap.balances(exchange_j) > 1e-13
111+
and exchange_amount_in / self.swap.balances(exchange_i) > 1e-13
112+
):
113+
raise
114+
return False
115+
116+
# This is to check that we didn't end up in a borked state after
117+
# an exchange succeeded
118+
self.swap.get_dy(
119+
exchange_j,
120+
exchange_i,
121+
10 ** 16 * 10 ** self.decimals[exchange_j] // ([10 ** 18] + INITIAL_PRICES)[exchange_j],
122+
)
123+
124+
d_balance_i -= self.coins[exchange_i].balanceOf(user)
125+
d_balance_j -= self.coins[exchange_j].balanceOf(user)
126+
127+
assert d_balance_i == exchange_amount_in
128+
if check_out_amount:
129+
if check_out_amount is True:
130+
assert -d_balance_j == calc_amount, f"{-d_balance_j} vs {calc_amount}"
131+
else:
132+
assert abs(d_balance_j + calc_amount) < max(
133+
check_out_amount * calc_amount, 3
134+
), f"{-d_balance_j} vs {calc_amount}"
135+
136+
self.balances[exchange_i] += d_balance_i
137+
self.balances[exchange_j] += d_balance_j
138+
139+
return True
140+
141+
def rule_sleep(self, sleep_time):
142+
self.chain.sleep(sleep_time)
143+
144+
def invariant_balances(self):
145+
balances = [self.swap.balances(i) for i in range(2)]
146+
balances_of = [c.balanceOf(self.swap) for c in self.coins]
147+
for i in range(2):
148+
assert self.balances[i] == balances[i]
149+
assert self.balances[i] == balances_of[i]
150+
151+
def invariant_total_supply(self):
152+
assert self.total_supply == self.token.totalSupply()
153+
154+
def invariant_virtual_price(self):
155+
virtual_price = self.swap.virtual_price()
156+
xcp_profit = self.swap.xcp_profit()
157+
get_virtual_price = self.swap.get_virtual_price()
158+
159+
assert xcp_profit >= 10 ** 18 - 10
160+
assert virtual_price >= 10 ** 18 - 10
161+
assert get_virtual_price >= 10 ** 18 - 10
162+
163+
assert xcp_profit - self.xcp_profit > -3, f"{xcp_profit} vs {self.xcp_profit}"
164+
assert (virtual_price - 10 ** 18) * 2 - (
165+
xcp_profit - 10 ** 18
166+
) >= -5, f"vprice={virtual_price}, xcp_profit={xcp_profit}"
167+
assert abs(log(virtual_price / get_virtual_price)) < 1e-10
168+
169+
self.xcp_profit = xcp_profit
170+
print("INVARIANT UPDATED xcp_profit", self.xcp_profit)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import math
2+
3+
import boa
4+
import pytest
5+
from boa.test import strategy
6+
from hypothesis import given, settings
7+
8+
from tests.fixtures.pool import INITIAL_PRICES
9+
from tests.utils import simulation_int_many as sim
10+
from tests.utils.tokens import mint_for_testing
11+
12+
SETTINGS = {"max_examples": 100, "deadline": None}
13+
14+
15+
@pytest.fixture(scope="module")
16+
def test_deposit(swap, coins, user, fee_receiver):
17+
18+
quantities = [10**36 // p for p in INITIAL_PRICES] # $3M worth
19+
20+
for coin, q in zip(coins, quantities):
21+
mint_for_testing(coin, user, q)
22+
with boa.env.prank(user):
23+
coin.approve(swap, 2**256 - 1)
24+
25+
bal_before = boa.env.get_balance(swap.address)
26+
with boa.env.prank(user):
27+
swap.add_liquidity(quantities, 0)
28+
29+
# test if eth wasnt deposited:
30+
assert boa.env.get_balance(swap.address) == bal_before
31+
32+
token_balance = swap.balanceOf(user)
33+
assert (
34+
token_balance == swap.totalSupply() - swap.balanceOf(fee_receiver) > 0
35+
)
36+
assert abs(swap.get_virtual_price() / 1e18 - 1) < 1e-3

0 commit comments

Comments
 (0)