|
| 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) |
0 commit comments