From 44a7bf84e95d3664410dd1dbb0e214081c353cc0 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:11:00 +0200 Subject: [PATCH 01/72] bump gauge version --- contracts/main/LiquidityGauge.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/LiquidityGauge.vy b/contracts/main/LiquidityGauge.vy index 6174a6e7..710baa8f 100644 --- a/contracts/main/LiquidityGauge.vy +++ b/contracts/main/LiquidityGauge.vy @@ -89,7 +89,7 @@ MAX_REWARDS: constant(uint256) = 8 TOKENLESS_PRODUCTION: constant(uint256) = 40 WEEK: constant(uint256) = 604800 -VERSION: constant(String[8]) = "v6.0.0" # <- updated from v5.0.0 (adds `create_from_blueprint` pattern) +VERSION: constant(String[8]) = "v6.1.0" # <- updated from v6.0.0 (makes rewards semi-permissionless) EIP712_TYPEHASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") EIP2612_TYPEHASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") From 42bff31aa937637f03b90382c7c5ccd3a135566e Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:51:25 +0200 Subject: [PATCH 02/72] feat: add exchange_received_split experimental idea; fix: upkeep xcp oracle in remove_liquidity --- contracts/main/CurveTwocryptoOptimized.vy | 291 ++++++++++++++++++---- tests/unitary/pool/test_a_gamma.py | 4 +- 2 files changed, 246 insertions(+), 49 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 7cb52444..088a58e0 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -135,7 +135,7 @@ cached_price_oracle: uint256 # <------- Price target given by moving average. cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. last_prices: uint256 -last_prices_timestamp: public(uint256) +last_timestamp: public(uint256[2]) # idx 0 is for prices, idx 1 is for xcp. last_xcp: public(uint256) xcp_ma_time: public(uint256) @@ -151,6 +151,9 @@ future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. # and not set to 0. balances: public(uint256[N_COINS]) +spot_wallet_balances: public(HashMap[address, uint256[N_COINS]]) # <---- Spot +# Wallet is a hashmap that stores balances for users, should they wish +# to not do ERC20 token transfers immediately out of the pool contract. D: public(uint256) xcp_profit: public(uint256) xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees. @@ -246,7 +249,7 @@ def __init__( self.cached_price_scale = initial_price self.cached_price_oracle = initial_price self.last_prices = initial_price - self.last_prices_timestamp = block.timestamp + self.last_timestamp = [block.timestamp, block.timestamp] self.xcp_profit_a = 10**18 self.xcp_ma_time = 62324 # <--------- 12 hours default on contract start. @@ -349,6 +352,81 @@ def _transfer_out(_coin_idx: uint256, _amount: uint256, receiver: address): ) +# ------------- Token transfers in and out involving Spot Wallets ------------ +# NOTE: EXPERIMENTAL + +@internal +def _transfer_to_spot_wallet(_coin_idx: uint256, _amount: uint256, _account: address): + + account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] + + # Adjust balances before handling transfers: + self.balances[_coin_idx] -= _amount + account_balance[_coin_idx] += _amount + self.spot_wallet_balances[_account] = account_balance + + +@internal +def _transfer_from_spot_wallet(_coin_idx: uint256, _amount: uint256, _account: address): + + account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] + + # Adjust balances before handling transfers: + self.balances[_coin_idx] += _amount + account_balance[_coin_idx] -= _amount + self.spot_wallet_balances[_account] = account_balance + + +@external +@nonreentrant('lock') +def deposit_to_spot_wallet(_amounts: uint256[N_COINS], _account: address): + + # Adjust balances before handling transfers + account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] + + # Transfer out of spot wallet + coin_balance: uint256 = 0 + for i in range(N_COINS): + + if _amounts[i] > 0: + + coin_balance = ERC20(coins[i]).balanceOf(self) + + assert ERC20(coins[i]).transferFrom( + _account, + self, + _amounts[i], + default_return_value=True + ) + + account_balance[i] += ERC20(coins[i]).balanceOf(self) - coin_balance + + # Update spot wallet account balances + self.spot_wallet_balances[_account] = account_balance + + +@external +@nonreentrant('lock') +def withdraw_from_spot_wallet(_amounts: uint256[N_COINS], _account: address): + + # Adjust balances before handling transfers + account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] + account_balance[0] -= _amounts[0] + account_balance[1] -= _amounts[1] + self.spot_wallet_balances[_account] = account_balance + + # Transfer out of spot wallet + for i in range(N_COINS): + + if _amounts[i] > 0: + + assert ERC20(coins[i]).transfer( + _account, + _amounts[i], + default_return_value=True + ) + + # -------------------------- AMM Main Functions ------------------------------ @@ -370,16 +448,31 @@ def exchange( @param receiver Address to send the output coin to. Default is msg.sender @return uint256 Amount of tokens at index j received by the `receiver """ - return self._exchange( + # _transfer_in updates self.balances here: + dx_received: uint256 = self._transfer_in( + i, + dx, msg.sender, + False + ) + + # No ERC20 token transfers occur here: + out: uint256[3] = self._exchange( i, j, - dx, + dx_received, min_dy, - receiver, - False, ) + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(j, out[0], receiver) + + # log: + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) + + return out[0] + @external @nonreentrant('lock') @@ -404,16 +497,110 @@ def exchange_received( @param receiver Address to send the output coin to @return uint256 Amount of tokens at index j received by the `receiver` """ - return self._exchange( + # _transfer_in updates self.balances here: + dx_received: uint256 = self._transfer_in( + i, + dx, msg.sender, + True # <---- expect_optimistic_transfer is set to True here. + ) + + # No ERC20 token transfers occur here: + out: uint256[3] = self._exchange( i, j, - dx, + dx_received, + min_dy, + ) + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(j, out[0], receiver) + + # log: + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) + + return out[0] + + +# NOTE: EXPERIMENTAL +@external +@nonreentrant('lock') +def exchange_received_split( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + split: uint256[2], + receiver: address, + use_spot_balance: bool, + expect_optimistic_transfer: bool +) -> uint256: + """ + @notice Exchange: but user must transfer dx amount of coin[i] tokens to pool first. + Pool will not call transferFrom and will only check if a surplus of + coins[i] is greater than or equal to `dx`. + User also needs to specify a `split` to decide what amount of dy_out goes + to receiver address and what amount stays in the sender's spot wallet. + @dev Use-case is to reduce the number of redundant ERC20 token + transfers in zaps. Primarily for dex-aggregators/arbitrageurs/searchers, + and helps reduce the total number of ERC20 tokens per arb transaction to + 2: arbitrageur can withdraw profits at a later time and do not need to hedge + atomically (which is expensive). + Note for users: please transfer + exchange_received in 1 tx. + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param split + @param receiver Address to send the output coin to + @param use_spot_balance + @param expect_optimistic_transfer + @return uint256 Amount of tokens at index j received by the `receiver` + """ + # _transfer_in updates self.balances here: + dx_received: uint256 = 0 + if not use_spot_balance: + # Two cases: + # 1. expect_optimistic_transfer is set to True. Use case: flashswap from + # another AMM, set this twocrypto-ng pool as receiver. + # 2. pool calls ERC20(coins[i]).transferFrom(msg.sender, self, dx) + dx_received = self._transfer_in( + i, + dx, + msg.sender, + expect_optimistic_transfer + ) + else: + # Only use balances in msg.sender's spot wallet account: + self._transfer_from_spot_wallet(i, dx, msg.sender) + dx_received = dx + + # No ERC20 token transfers occur here: + out: uint256[3] = self._exchange( + i, + j, + dx_received, min_dy, - receiver, - True, ) + assert split[0] + split[1] == out[0] # dev: requested split is greater than calculated dy + + # Difference between calculated dy and requested out amount is what stays + # in the spot wallet. To make this a fully spot_wallet swap, set split[0] to 0. + if split[1] > 0: + self._transfer_to_spot_wallet(j, split[1], msg.sender) + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + if split[0] > 0: + self._transfer_out(j, split[0], receiver) + + # log: + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) + + return out[0] + @external @nonreentrant("lock") @@ -600,6 +787,35 @@ def remove_liquidity( log RemoveLiquidity(msg.sender, withdraw_amounts, total_supply - _amount) + # --------------------------- Upkeep xcp oracle -------------------------- + + # Update xcp since liquidity was removed: + xp: uint256[N_COINS] = self.xp(self.balances, self.cached_price_scale) + self.last_xcp = isqrt(xp[0] * xp[1] / 10**18) + + last_timestamp: uint256[2] = self.last_timestamp + if last_timestamp[1] < block.timestamp: + + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + alpha: uint256 = MATH.wad_exp( + -convert( + unsafe_div( + (block.timestamp - last_timestamp[1]) * 10**18, + self.xcp_ma_time # <---------- xcp ma time has is longer. + ), + int256, + ) + ) + + self.cached_xcp_oracle = unsafe_div( + self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, + 10**18 + ) + last_timestamp[1] = block.timestamp + + # Pack and store timestamps: + self.last_timestamp = last_timestamp + return withdraw_amounts @@ -698,17 +914,14 @@ def _unpack(_packed: uint256) -> uint256[3]: @internal def _exchange( - sender: address, i: uint256, j: uint256, - dx: uint256, + dx_received: uint256, min_dy: uint256, - receiver: address, - expect_optimistic_transfer: bool, -) -> uint256: +) -> uint256[3]: assert i != j # dev: coin index out of range - assert dx > 0 # dev: do not exchange 0 coins + assert dx_received > 0 # dev: do not exchange 0 coins A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances @@ -717,16 +930,6 @@ def _exchange( y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert. x0: uint256 = xp[i] # <--------------- if i > N_COINS, this will revert. - ########################## TRANSFER IN <------- - - # _transfer_in updates self.balances here: - dx_received: uint256 = self._transfer_in( - i, - dx, - sender, - expect_optimistic_transfer # <---- If True, pool expects dx tokens to - ) # be transferred in. - xp[i] = x0 + dx_received price_scale: uint256 = self.cached_price_scale @@ -779,17 +982,7 @@ def _exchange( price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1]) - # --------------------------- Do Transfers out --------------------------- - - ########################## -------> TRANSFER OUT - - # _transfer_out updates self.balances here. Update to state occurs before - # external calls: - self._transfer_out(j, dy, receiver) - - log TokenExchange(sender, i, dx_received, j, dy, fee, price_scale) - - return dy + return [dy, fee, price_scale] @internal @@ -825,8 +1018,9 @@ def tweak_price( # ----------------------- Update Oracles if needed ----------------------- - last_timestamp: uint256 = self.last_prices_timestamp - if last_timestamp < block.timestamp: # 0th index is for price_oracle. + last_timestamp: uint256[2] = self.last_timestamp + alpha: uint256 = 0 + if last_timestamp[0] < block.timestamp: # 0th index is for price_oracle. # The moving average price oracle is calculated using the last_price # of the trade at the previous block, and the price oracle logged @@ -834,10 +1028,10 @@ def tweak_price( # ------------------ Calculate moving average params ----------------- - alpha: uint256 = MATH.wad_exp( + alpha = MATH.wad_exp( -convert( unsafe_div( - (block.timestamp - last_timestamp) * 10**18, + (block.timestamp - last_timestamp[0]) * 10**18, rebalancing_params[2] # <----------------------- ma_time. ), int256, @@ -855,14 +1049,17 @@ def tweak_price( ) self.cached_price_oracle = price_oracle + last_timestamp[0] = block.timestamp - # ------------------------------------------------- Update xcp oracle. + # ----------------------------------------------------- Update xcp oracle. + + if last_timestamp[1] < block.timestamp: cached_xcp_oracle: uint256 = self.cached_xcp_oracle alpha = MATH.wad_exp( -convert( unsafe_div( - (block.timestamp - last_timestamp) * 10**18, + (block.timestamp - last_timestamp[1]) * 10**18, self.xcp_ma_time # <---------- xcp ma time has is longer. ), int256, @@ -875,7 +1072,9 @@ def tweak_price( ) # Pack and store timestamps: - self.last_prices_timestamp = block.timestamp + last_timestamp[1] = block.timestamp + + self.last_timestamp = last_timestamp # `price_oracle` is used further on to calculate its vector distance from # price_scale. This distance is used to calculate the amount of adjustment @@ -1547,7 +1746,7 @@ def price_oracle() -> uint256: """ price_oracle: uint256 = self.cached_price_oracle price_scale: uint256 = self.cached_price_scale - last_prices_timestamp: uint256 = self.last_prices_timestamp + last_prices_timestamp: uint256 = self.last_timestamp[0] if last_prices_timestamp < block.timestamp: # <------------ Update moving # average if needed. @@ -1585,7 +1784,7 @@ def xcp_oracle() -> uint256: @return uint256 Oracle value of xcp. """ - last_prices_timestamp: uint256 = self.last_prices_timestamp + last_prices_timestamp: uint256 = self.last_timestamp[1] cached_xcp_oracle: uint256 = self.cached_xcp_oracle if last_prices_timestamp < block.timestamp: diff --git a/tests/unitary/pool/test_a_gamma.py b/tests/unitary/pool/test_a_gamma.py index 07966400..0e43517f 100644 --- a/tests/unitary/pool/test_a_gamma.py +++ b/tests/unitary/pool/test_a_gamma.py @@ -1,9 +1,7 @@ import boa -def test_A_gamma(swap, math_contract, params): - - breakpoint() +def test_A_gamma(swap, params): A = swap.A() gamma = swap.gamma() From b0aecd0ef73ae618e72dbd799994be3d57187b36 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:56:29 +0200 Subject: [PATCH 03/72] add docstrings to exchange_received_split --- contracts/main/CurveTwocryptoOptimized.vy | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 088a58e0..140b3220 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -552,10 +552,15 @@ def exchange_received_split( @param j Index value for the output coin @param dx Amount of input coin being swapped in @param min_dy Minimum amount of output coin to receive - @param split + @param split Array of output amounts that are handled by the pool. There are two + elements in the array: index 0 is the amount of coin[j] sent out to + `receiver`. The rest goes into msg.sender's spot wallet balances. @param receiver Address to send the output coin to - @param use_spot_balance - @param expect_optimistic_transfer + @param use_spot_balance If True, do not do ERC20 token transfers and only use + tokens in user's spot wallet account. + @param expect_optimistic_transfer If True: user needs to do a transfer into the pool + similar to exchange_received, and then call this + method. @return uint256 Amount of tokens at index j received by the `receiver` """ # _transfer_in updates self.balances here: From be077a29d397ec706c9044fd67f37538948e5f91 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 6 Oct 2023 13:31:35 +0200 Subject: [PATCH 04/72] add distinction between split_in and split_out --- contracts/main/CurveTwocryptoOptimized.vy | 37 +++++++++++++---------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 140b3220..acba0d7b 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -529,11 +529,10 @@ def exchange_received( def exchange_received_split( i: uint256, j: uint256, - dx: uint256, + split_in: uint256[2], # <---- sum of split_in is `dx` (amount_in) min_dy: uint256, - split: uint256[2], + split_out: uint256[2], receiver: address, - use_spot_balance: bool, expect_optimistic_transfer: bool ) -> uint256: """ @@ -550,9 +549,15 @@ def exchange_received_split( Note for users: please transfer + exchange_received in 1 tx. @param i Index value for the input coin @param j Index value for the output coin - @param dx Amount of input coin being swapped in + @param split_in Amount of input coin being swapped in. + Index 0 is what is transferred in from outside the pool contract. + This can occur using either transferFrom if expect_optimistic_transfer + is set to False, else the pool contract expects user to transfer into + the pool (in the same tx) and then call exchange_received_split (with + expect_optimistic_transfer set to True). + Index 1 is what is used from msg.sender's spot balance. @param min_dy Minimum amount of output coin to receive - @param split Array of output amounts that are handled by the pool. There are two + @param split_out Array of output amounts that are handled by the pool. There are two elements in the array: index 0 is the amount of coin[j] sent out to `receiver`. The rest goes into msg.sender's spot wallet balances. @param receiver Address to send the output coin to @@ -565,21 +570,21 @@ def exchange_received_split( """ # _transfer_in updates self.balances here: dx_received: uint256 = 0 - if not use_spot_balance: + if split_in[0] > 0: # Two cases: # 1. expect_optimistic_transfer is set to True. Use case: flashswap from # another AMM, set this twocrypto-ng pool as receiver. # 2. pool calls ERC20(coins[i]).transferFrom(msg.sender, self, dx) dx_received = self._transfer_in( i, - dx, + split_in[0], msg.sender, expect_optimistic_transfer ) - else: - # Only use balances in msg.sender's spot wallet account: - self._transfer_from_spot_wallet(i, dx, msg.sender) - dx_received = dx + + if split_in[1] > 0: + self._transfer_from_spot_wallet(i, split_in[1], msg.sender) + dx_received += split_in[1] # No ERC20 token transfers occur here: out: uint256[3] = self._exchange( @@ -589,17 +594,17 @@ def exchange_received_split( min_dy, ) - assert split[0] + split[1] == out[0] # dev: requested split is greater than calculated dy + assert split_out[0] + split_out[1] == out[0] # dev: requested split is greater than calculated dy # Difference between calculated dy and requested out amount is what stays # in the spot wallet. To make this a fully spot_wallet swap, set split[0] to 0. - if split[1] > 0: - self._transfer_to_spot_wallet(j, split[1], msg.sender) + if split_out[1] > 0: + self._transfer_to_spot_wallet(j, split_out[1], msg.sender) # _transfer_out updates self.balances here. Update to state occurs before # external calls: - if split[0] > 0: - self._transfer_out(j, split[0], receiver) + if split_out[0] > 0: + self._transfer_out(j, split_out[0], receiver) # log: log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) From d3a628210a575bd10ed7e47a6cceefb57ac8d7d5 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:31:36 +0200 Subject: [PATCH 05/72] fix: isqrt precision issues; tests --- contracts/experimental/n=2.vy | 195 +++++++ contracts/experimental/n=2_optimized.vy | 368 +++++++++++++ contracts/main/CurveCryptoMathOptimized2.vy | 155 +++--- contracts/main/CurveCryptoViews2Optimized.vy | 42 +- contracts/main/CurveTwocryptoOptimized.vy | 37 +- tests/fixtures/pool.py | 15 +- tests/unitary/math/conftest.py | 14 + tests/unitary/math/misc.py | 39 ++ tests/unitary/math/test_get_y.py | 102 ++++ tests/unitary/math/test_newton_D.py | 241 +++++++++ tests/unitary/pool/test_deposit_withdraw.py | 378 +++++++++++++ tests/unitary/pool/test_exchange.py | 144 +++++ tests/utils/simulation_int_many.py | 533 +++++++++++++++++++ 13 files changed, 2148 insertions(+), 115 deletions(-) create mode 100644 contracts/experimental/n=2.vy create mode 100644 contracts/experimental/n=2_optimized.vy create mode 100644 tests/unitary/math/conftest.py create mode 100644 tests/unitary/math/misc.py create mode 100644 tests/unitary/math/test_get_y.py create mode 100644 tests/unitary/math/test_newton_D.py create mode 100644 tests/unitary/pool/test_deposit_withdraw.py create mode 100644 tests/unitary/pool/test_exchange.py create mode 100644 tests/utils/simulation_int_many.py diff --git a/contracts/experimental/n=2.vy b/contracts/experimental/n=2.vy new file mode 100644 index 00000000..e864537c --- /dev/null +++ b/contracts/experimental/n=2.vy @@ -0,0 +1,195 @@ +# @version 0.3.10 + + +N_COINS: constant(uint256) = 2 +PRECISION: constant(uint256) = 10 ** 18 # The precision to convert to +A_MULTIPLIER: constant(uint256) = 10000 + +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA: constant(uint256) = 2 * 10**16 + +MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 100000 + + +@internal +@pure +def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256: + """ + Calculating x[i] given other balances x[0..N_COINS-1] and invariant D + ANN = A * N**N + """ + # Safety checks + # assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A + # assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + # assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D + + x_j: uint256 = x[1 - i] + y: uint256 = D**2 / (x_j * N_COINS**2) + K0_i: uint256 = (10**18 * N_COINS) * x_j / D + # S_i = x_j + + # frac = x_j * 1e18 / D => frac = K0_i / N_COINS + # assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i] + + # x_sorted: uint256[N_COINS] = x + # x_sorted[i] = 0 + # x_sorted = self.sort(x_sorted) # From high to low + # x[not i] instead of x_sorted since x_soted has only 1 element + + convergence_limit: uint256 = max(max(x_j / 10**14, D / 10**14), 100) + + for j in range(255): + y_prev: uint256 = y + + K0: uint256 = K0_i * y * N_COINS / D + S: uint256 = x_j + y + + _g1k0: uint256 = gamma + 10**18 + if _g1k0 > K0: + _g1k0 = _g1k0 - K0 + 1 + else: + _g1k0 = K0 - _g1k0 + 1 + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1: uint256 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + + # 2*K0 / _g1k0 + mul2: uint256 = 10**18 + (2 * 10**18) * K0 / _g1k0 + + yfprime: uint256 = 10**18 * y + S * mul2 + mul1 + _dyfprime: uint256 = D * mul2 + if yfprime < _dyfprime: + y = y_prev / 2 + continue + else: + yfprime -= _dyfprime + fprime: uint256 = yfprime / y + + # y -= f / f_prime; y = (y * fprime - f) / fprime + # y = (yfprime + 10**18 * D - 10**18 * S) // fprime + mul1 // fprime * (10**18 - K0) // K0 + y_minus: uint256 = mul1 / fprime + y_plus: uint256 = (yfprime + 10**18 * D) / fprime + y_minus * 10**18 / K0 + y_minus += 10**18 * S / fprime + + if y_plus < y_minus: + y = y_prev / 2 + else: + y = y_plus - y_minus + + diff: uint256 = 0 + if y > y_prev: + diff = y - y_prev + else: + diff = y_prev - y + if diff < max(convergence_limit, y / 10**14): + frac: uint256 = y * 10**18 / D + # assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y + return y + + raise "Did not converge" + +@external +@pure +def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256: + return self._newton_y(ANN, gamma, x, D, i) + + +@internal +@pure +def geometric_mean(unsorted_x: uint256[N_COINS], sort: bool) -> uint256: + """ + (x[0] * x[1] * ...) ** (1/N) + """ + x: uint256[N_COINS] = unsorted_x + if sort and x[0] < x[1]: + x = [unsorted_x[1], unsorted_x[0]] + D: uint256 = x[0] + diff: uint256 = 0 + for i in range(255): + D_prev: uint256 = D + # tmp: uint256 = 10**18 + # for _x in x: + # tmp = tmp * _x / D + # D = D * ((N_COINS - 1) * 10**18 + tmp) / (N_COINS * 10**18) + # line below makes it for 2 coins + D = unsafe_div(D + x[0] * x[1] / D, N_COINS) + if D > D_prev: + diff = unsafe_sub(D, D_prev) + else: + diff = unsafe_sub(D_prev, D) + if diff <= 1 or diff * 10**18 < D: + return D + raise "Did not converge" + + +@external +@view +def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS]) -> uint256: + """ + Finding the invariant using Newton method. + ANN is higher by the factor A_MULTIPLIER + ANN is already A * N**N + + Currently uses 60k gas + """ + + # Initial value of invariant D is that for constant-product invariant + x: uint256[N_COINS] = x_unsorted + if x[0] < x[1]: + x = [x_unsorted[1], x_unsorted[0]] + + D: uint256 = N_COINS * self.geometric_mean(x, False) + S: uint256 = x[0] + x[1] + __g1k0: uint256 = gamma + 10**18 + + for i in range(255): + D_prev: uint256 = D + assert D > 0 + # Unsafe ivision by D is now safe + + # K0: uint256 = 10**18 + # for _x in x: + # K0 = K0 * _x * N_COINS / D + # collapsed for 2 coins + K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D) + + _g1k0: uint256 = __g1k0 + if _g1k0 > K0: + _g1k0 = unsafe_sub(_g1k0, K0) + 1 # > 0 + else: + _g1k0 = unsafe_sub(K0, _g1k0) + 1 # > 0 + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN) + + # 2*N*K0 / _g1k0 + mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0) + + neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18) + + # D -= f / fprime + D_plus: uint256 = D * (neg_fprime + S) / neg_fprime + D_minus: uint256 = D*D / neg_fprime + if 10**18 > K0: + D_minus += unsafe_div(D * (mul1 / neg_fprime), 10**18) * unsafe_sub(10**18, K0) / K0 + else: + D_minus -= unsafe_div(D * (mul1 / neg_fprime), 10**18) * unsafe_sub(K0, 10**18) / K0 + + if D_plus > D_minus: + D = unsafe_sub(D_plus, D_minus) + else: + D = unsafe_div(unsafe_sub(D_minus, D_plus), 2) + + diff: uint256 = 0 + if D > D_prev: + diff = unsafe_sub(D, D_prev) + else: + diff = unsafe_sub(D_prev, D) + if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here + # Test that we are safe with the next newton_y + for _x in x: + frac: uint256 = _x * 10**18 / D + return D + + raise "Did not converge" diff --git a/contracts/experimental/n=2_optimized.vy b/contracts/experimental/n=2_optimized.vy new file mode 100644 index 00000000..69a8ef13 --- /dev/null +++ b/contracts/experimental/n=2_optimized.vy @@ -0,0 +1,368 @@ +# @version 0.3.10 + + +N_COINS: constant(uint256) = 2 +PRECISION: constant(uint256) = 10 ** 18 # The precision to convert to +A_MULTIPLIER: constant(uint256) = 10000 + + +@internal +@pure +def _cbrt(x: uint256) -> uint256: + + # we artificially set a cap to the values for which we can compute the + # cube roots safely. This is not to say that there are no values above + # max(uint256) // 10**36 for which we cannot get good cube root estimates. + # However, beyond this point, accuracy is not guaranteed since overflows + # start to occur. + # assert x < 115792089237316195423570985008687907853269, "inaccurate cbrt" # TODO: check limits again + + # we increase precision of input `x` by multiplying 10 ** 36. + # in such cases: cbrt(10**18) = 10**18, cbrt(1) = 10**12 + xx: uint256 = 0 + if x >= 115792089237316195423570985008687907853269 * 10**18: + xx = x + elif x >= 115792089237316195423570985008687907853269: + xx = unsafe_mul(x, 10**18) + else: + xx = unsafe_mul(x, 10**36) + + # get log2(x) for approximating initial value + # logic is: cbrt(a) = cbrt(2**(log2(a))) = 2**(log2(a) / 3) ≈ 2**|log2(a)/3| + # from: https://github.com/transmissions11/solmate/blob/b9d69da49bbbfd090f1a73a4dba28aa2d5ee199f/src/utils/FixedPointMathLib.sol#L352 + + a_pow: int256 = 0 + if xx > 340282366920938463463374607431768211455: + a_pow = 128 + if unsafe_div(xx, shift(2, a_pow)) > 18446744073709551615: + a_pow = a_pow | 64 + if unsafe_div(xx, shift(2, a_pow)) > 4294967295: + a_pow = a_pow | 32 + if unsafe_div(xx, shift(2, a_pow)) > 65535: + a_pow = a_pow | 16 + if unsafe_div(xx, shift(2, a_pow)) > 255: + a_pow = a_pow | 8 + if unsafe_div(xx, shift(2, a_pow)) > 15: + a_pow = a_pow | 4 + if unsafe_div(xx, shift(2, a_pow)) > 3: + a_pow = a_pow | 2 + if unsafe_div(xx, shift(2, a_pow)) > 1: + a_pow = a_pow | 1 + + # initial value: 2**|log2(a)/3| + # which is: 2 ** (n / 3) * 1260 ** (n % 3) / 1000 ** (n % 3) + a_pow_mod: uint256 = convert(a_pow, uint256) % 3 + a: uint256 = unsafe_div( + unsafe_mul( + pow_mod256( + 2, + unsafe_div( + convert(a_pow, uint256), 3 + ) + ), + pow_mod256(1260, a_pow_mod) + ), + pow_mod256(1000, a_pow_mod) + ) + + # 7 newton raphson iterations: + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + + if x >= 115792089237316195423570985008687907853269 * 10**18: + return a*10**12 + elif x >= 115792089237316195423570985008687907853269: + return a*10**6 + else: + return a + + +@internal +@pure +def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256: + """ + Calculating x[i] given other balances x[0..N_COINS-1] and invariant D + ANN = A * N**N + """ + # Safety checks + # assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A + # assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + # assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D + + x_j: uint256 = x[1 - i] + y: uint256 = D**2 / (x_j * N_COINS**2) + K0_i: uint256 = (10**18 * N_COINS) * x_j / D + # S_i = x_j + + # frac = x_j * 1e18 / D => frac = K0_i / N_COINS + # assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i] + + # x_sorted: uint256[N_COINS] = x + # x_sorted[i] = 0 + # x_sorted = self.sort(x_sorted) # From high to low + # x[not i] instead of x_sorted since x_soted has only 1 element + + convergence_limit: uint256 = max(max(x_j / 10**14, D / 10**14), 100) + + for j in range(255): + y_prev: uint256 = y + + K0: uint256 = K0_i * y * N_COINS / D + S: uint256 = x_j + y + + _g1k0: uint256 = gamma + 10**18 + if _g1k0 > K0: + _g1k0 = _g1k0 - K0 + 1 + else: + _g1k0 = K0 - _g1k0 + 1 + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1: uint256 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + + # 2*K0 / _g1k0 + mul2: uint256 = 10**18 + (2 * 10**18) * K0 / _g1k0 + + yfprime: uint256 = 10**18 * y + S * mul2 + mul1 + _dyfprime: uint256 = D * mul2 + if yfprime < _dyfprime: + y = y_prev / 2 + continue + else: + yfprime -= _dyfprime + fprime: uint256 = yfprime / y + + # y -= f / f_prime; y = (y * fprime - f) / fprime + # y = (yfprime + 10**18 * D - 10**18 * S) // fprime + mul1 // fprime * (10**18 - K0) // K0 + y_minus: uint256 = mul1 / fprime + y_plus: uint256 = (yfprime + 10**18 * D) / fprime + y_minus * 10**18 / K0 + y_minus += 10**18 * S / fprime + + if y_plus < y_minus: + y = y_prev / 2 + else: + y = y_plus - y_minus + + diff: uint256 = 0 + if y > y_prev: + diff = y - y_prev + else: + diff = y_prev - y + if diff < max(convergence_limit, y / 10**14): + frac: uint256 = y * 10**18 / D + # assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y + return y + + raise "Did not converge" + +@external +@pure +def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256: + return self._newton_y(ANN, gamma, x, D, i) + + +@external +@pure +def get_y(_ANN: uint256, _gamma: uint256, _x: uint256[N_COINS], _D: uint256, i: uint256) -> uint256[2]: + + # Safety checks go here + + j: uint256 = 0 + if i == 0: + j = 1 + elif i == 1: + j = 0 + + ANN: int256 = convert(_ANN, int256) + gamma: int256 = convert(_gamma, int256) + D: int256 = convert(_D, int256) + x_j: int256 = convert(_x[j], int256) + gamma2: int256 = gamma**2 + + a: int256 = 10**32 + b: int256 = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14 + c: int256 = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4 + d: int256 = -(10**18+gamma)**2 / 10**4 + + delta0: int256 = 3*a*c/b - b + delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b + + divider: int256 = 0 + threshold: int256 = min(min(abs(delta0), abs(delta1)), a) + if threshold > 10**48: + divider = 10**30 + elif threshold > 10**46: + divider = 10**28 + elif threshold > 10**44: + divider = 10**26 + elif threshold > 10**42: + divider = 10**24 + elif threshold > 10**40: + divider = 10**22 + elif threshold > 10**38: + divider = 10**20 + elif threshold > 10**36: + divider = 10**18 + elif threshold > 10**34: + divider = 10**16 + elif threshold > 10**32: + divider = 10**14 + elif threshold > 10**30: + divider = 10**12 + elif threshold > 10**28: + divider = 10**10 + elif threshold > 10**26: + divider = 10**8 + elif threshold > 10**24: + divider = 10**6 + elif threshold > 10**20: + divider = 10**2 + else: + divider = 1 + + a /= divider + b /= divider + c /= divider + d /= divider + + delta0 = 3*a*c/b - b + delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b + + sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0 + sqrt_val: int256 = 0 + if sqrt_arg > 0: + sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256) + else: + return [self._newton_y(_ANN, _gamma, _x, _D, i), 0] + + b_cbrt: int256 = 0 + if b > 0: + b_cbrt = convert(self._cbrt(convert(b, uint256)), int256) + else: + b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256) + + second_cbrt: int256 = 0 + if delta1 > 0: + second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256) + else: + second_cbrt = -convert(self._cbrt(convert(-(delta1 - sqrt_val), uint256) / 2), int256) + + C1: int256 = b_cbrt*b_cbrt/10**18*second_cbrt/10**18 + + root: int256 = -(10**18*b - 10**18*C1 + 10**18*b*delta0/C1)/(3*a) + + return [convert(D**2/x_j*root/4/10**18, uint256), convert(root, uint256)] + + +@internal +@pure +def geometric_mean(unsorted_x: uint256[N_COINS], sort: bool) -> uint256: + """ + (x[0] * x[1] * ...) ** (1/N) + """ + x: uint256[N_COINS] = unsorted_x + if sort and x[0] < x[1]: + x = [unsorted_x[1], unsorted_x[0]] + D: uint256 = x[0] + diff: uint256 = 0 + for i in range(255): + D_prev: uint256 = D + # tmp: uint256 = 10**18 + # for _x in x: + # tmp = tmp * _x / D + # D = D * ((N_COINS - 1) * 10**18 + tmp) / (N_COINS * 10**18) + # line below makes it for 2 coins + D = unsafe_div(D + x[0] * x[1] / D, N_COINS) + if D > D_prev: + diff = unsafe_sub(D, D_prev) + else: + diff = unsafe_sub(D_prev, D) + if diff <= 1 or diff * 10**18 < D: + return D + raise "Did not converge" + + +@external +@view +def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256: + """ + Finding the invariant using Newton method. + ANN is higher by the factor A_MULTIPLIER + ANN is already A * N**N + + Currently uses 60k gas + """ + + # Initial value of invariant D is that for constant-product invariant + x: uint256[N_COINS] = x_unsorted + if x[0] < x[1]: + x = [x_unsorted[1], x_unsorted[0]] + + S: uint256 = x[0] + x[1] + + D: uint256 = 0 + if K0_prev == 0: + # Geometric mean of 3 numbers cannot be larger than the largest number + # so the following is safe to do: + D = N_COINS * self.geometric_mean(x, False) + else: + D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18) + if S < D: + D = S + + __g1k0: uint256 = gamma + 10**18 + + for i in range(255): + D_prev: uint256 = D + assert D > 0 + # Unsafe ivision by D is now safe + + # K0: uint256 = 10**18 + # for _x in x: + # K0 = K0 * _x * N_COINS / D + # collapsed for 2 coins + K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D) + + _g1k0: uint256 = __g1k0 + if _g1k0 > K0: + _g1k0 = unsafe_sub(_g1k0, K0) + 1 # > 0 + else: + _g1k0 = unsafe_sub(K0, _g1k0) + 1 # > 0 + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN) + + # 2*N*K0 / _g1k0 + mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0) + + neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18) + + # D -= f / fprime + D_plus: uint256 = D * (neg_fprime + S) / neg_fprime + D_minus: uint256 = D*D / neg_fprime + if 10**18 > K0: + D_minus += unsafe_div(D * (mul1 / neg_fprime), 10**18) * unsafe_sub(10**18, K0) / K0 + else: + D_minus -= unsafe_div(D * (mul1 / neg_fprime), 10**18) * unsafe_sub(K0, 10**18) / K0 + + if D_plus > D_minus: + D = unsafe_sub(D_plus, D_minus) + else: + D = unsafe_div(unsafe_sub(D_minus, D_plus), 2) + + diff: uint256 = 0 + if D > D_prev: + diff = unsafe_sub(D, D_prev) + else: + diff = unsafe_sub(D_prev, D) + if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here + # Test that we are safe with the next newton_y + for _x in x: + frac: uint256 = _x * 10**18 / D + return D + + raise "Did not converge" diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index fd399394..1eef9a35 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -37,7 +37,8 @@ def _cbrt(x: uint256) -> uint256: # max(uint256) // 10**36 for which we cannot get good cube root estimates. # However, beyond this point, accuracy is not guaranteed since overflows # start to occur. - # assert x < 115792089237316195423570985008687907853269, "inaccurate cbrt" # TODO: check limits again + + assert x < 115792089237316195423570985008687907853269, "inaccurate cbrt" # we increase precision of input `x` by multiplying 10 ** 36. # in such cases: cbrt(10**18) = 10**18, cbrt(1) = 10**12 @@ -110,24 +111,14 @@ def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: """ Calculating x[i] given other balances x[0..N_COINS-1] and invariant D ANN = A * N**N + This is computationally expensive. """ - # Safety checks - # assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A - # assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma - # assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D x_j: uint256 = x[1 - i] y: uint256 = D**2 / (x_j * N_COINS**2) K0_i: uint256 = (10**18 * N_COINS) * x_j / D - # S_i = x_j - - # frac = x_j * 1e18 / D => frac = K0_i / N_COINS - # assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i] - # x_sorted: uint256[N_COINS] = x - # x_sorted[i] = 0 - # x_sorted = self.sort(x_sorted) # From high to low - # x[not i] instead of x_sorted since x_soted has only 1 element + assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i] convergence_limit: uint256 = max(max(x_j / 10**14, D / 10**14), 100) @@ -174,48 +165,83 @@ def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: diff = y - y_prev else: diff = y_prev - y + if diff < max(convergence_limit, y / 10**14): - frac: uint256 = y * 10**18 / D - # assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y return y raise "Did not converge" + @external @pure def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256: - return self._newton_y(ANN, gamma, x, D, i) + + # Safety checks + assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A + assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D + + y: uint256 = self._newton_y(ANN, gamma, x, D, i) + frac: uint256 = y * 10**18 / D + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y + + return y @external @pure -def get_y(_ANN: uint256, _gamma: uint256, _x: uint256[N_COINS], _D: uint256, i: uint256) -> uint256[2]: - - # Safety checks go here +def get_y( + _ANN: uint256, + _gamma: uint256, + _x: uint256[N_COINS], + _D: uint256, + i: uint256 +) -> uint256[2]: - j: uint256 = 0 - if i == 0: - j = 1 - elif i == 1: - j = 0 + # Safety checks + assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A + assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D ANN: int256 = convert(_ANN, int256) gamma: int256 = convert(_gamma, int256) D: int256 = convert(_D, int256) - x_j: int256 = convert(_x[j], int256) - gamma2: int256 = gamma**2 + x_j: int256 = convert(_x[1 - i], int256) + gamma2: int256 = unsafe_mul(gamma, gamma) + + y: int256 = D**2 / (x_j * N_COINS**2) + K0_i: int256 = (10**18 * N_COINS) * x_j / D + assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i] + # a = 10**36 / N_COINS**2 a: int256 = 10**32 - b: int256 = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14 - c: int256 = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4 + + # b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14 + b: int256 = ( + ANN*D*gamma2/4/10000/x_j/10**4 + - convert(unsafe_mul(10**32, 3), int256) + - unsafe_mul(unsafe_mul(2, gamma), 10**14) + ) + + # c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4 + c: int256 = ( + 10**32*3 + + 4*gamma*10**14 + + gamma2/10**4 + + 4*ANN*gamma2*x_j/D/10000/4/10**4 + - 4*ANN*gamma2/10000/4/10**4 + ) + + # d = -(10**18+gamma)**2 / 10**4 d: int256 = -(10**18+gamma)**2 / 10**4 # delta0: int256 = 3*a*c/b - b - delta0: int256 = unsafe_sub(unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b), b) + delta0: int256 = 3*a*c/b - b + # delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b - delta1: int256 = unsafe_div(unsafe_mul(unsafe_mul(9, a), c), b) - unsafe_mul(2, b) - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b) + delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b - divider: int256 = 0 + divider: int256 = 1 threshold: int256 = min(min(abs(delta0), abs(delta1)), a) if threshold > 10**48: divider = 10**30 @@ -245,8 +271,6 @@ def get_y(_ANN: uint256, _gamma: uint256, _x: uint256[N_COINS], _D: uint256, i: divider = 10**6 elif threshold > 10**20: divider = 10**2 - else: - divider = 1 a = unsafe_div(a, divider) b = unsafe_div(b, divider) @@ -254,9 +278,10 @@ def get_y(_ANN: uint256, _gamma: uint256, _x: uint256[N_COINS], _D: uint256, i: d = unsafe_div(d, divider) # delta0 = 3*a*c/b - b - delta0 = unsafe_sub(unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b), b) + delta0 = 3*a*c/b - b + # delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b - delta1 = unsafe_div(unsafe_mul(unsafe_mul(9, a), c), b) - unsafe_mul(2, b) - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b) + delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b # sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0 sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0) @@ -264,7 +289,10 @@ def get_y(_ANN: uint256, _gamma: uint256, _x: uint256[N_COINS], _D: uint256, i: if sqrt_arg > 0: sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256) else: - return [self._newton_y(_ANN, _gamma, _x, _D, i), 0] + return [ + self._newton_y(_ANN, _gamma, _x, _D, i), + 0 + ] b_cbrt: int256 = 0 if b > 0: @@ -286,36 +314,16 @@ def get_y(_ANN: uint256, _gamma: uint256, _x: uint256[N_COINS], _D: uint256, i: # root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here. root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_div(unsafe_mul(10**18, b)*delta0, C1))/unsafe_mul(3, a) - # return [convert(D**2/x_j*root/4/10**18, uint256), convert(root, uint256)] - return [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)] + # return [ + # convert(D**2/x_j*root/4/10**18, uint256), # <--- y + # convert(root, uint256) # <----------------------- K0Prev + # ] + y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)] + frac: uint256 = y_out[0] * 10**18 / _D + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y -@internal -@pure -def geometric_mean(unsorted_x: uint256[N_COINS], sort: bool) -> uint256: - """ - (x[0] * x[1] * ...) ** (1/N) - """ - x: uint256[N_COINS] = unsorted_x - if sort and x[0] < x[1]: - x = [unsorted_x[1], unsorted_x[0]] - D: uint256 = x[0] - diff: uint256 = 0 - for i in range(255): - D_prev: uint256 = D - # tmp: uint256 = 10**18 - # for _x in x: - # tmp = tmp * _x / D - # D = D * ((N_COINS - 1) * 10**18 + tmp) / (N_COINS * 10**18) - # line below makes it for 2 coins - D = unsafe_div(D + x[0] * x[1] / D, N_COINS) - if D > D_prev: - diff = unsafe_sub(D, D_prev) - else: - diff = unsafe_sub(D_prev, D) - if diff <= 1 or diff * 10**18 < D: - return D - raise "Did not converge" + return y_out @external @@ -325,15 +333,20 @@ def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev Finding the invariant using Newton method. ANN is higher by the factor A_MULTIPLIER ANN is already A * N**N - - Currently uses 60k gas """ + # Safety checks + assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A + assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + # Initial value of invariant D is that for constant-product invariant x: uint256[N_COINS] = x_unsorted if x[0] < x[1]: x = [x_unsorted[1], x_unsorted[0]] + assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0] + assert x[1] * 10**18 / x[0] > 10**14-1 # dev: unsafe values x[i] (input) + S: uint256 = x[0] + x[1] D: uint256 = 0 @@ -390,10 +403,12 @@ def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev diff = unsafe_sub(D, D_prev) else: diff = unsafe_sub(D_prev, D) + if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here - # Test that we are safe with the next newton_y + for _x in x: frac: uint256 = _x * 10**18 / D + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i] return D raise "Did not converge" @@ -447,6 +462,6 @@ def get_p( # p_xz = x * (GK0 + NNAG2 * z / D * K0 / 10**36) / z * 10**18 / denominator # p is in 10**18 precision. return unsafe_div( - _xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[1], _D) * K0, 10**36) ) / _xp[1] * 10**18, - denominator - ) + _xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[1], _D) * K0, 10**36) ) / _xp[1] * 10**18, + denominator + ) diff --git a/contracts/main/CurveCryptoViews2Optimized.vy b/contracts/main/CurveCryptoViews2Optimized.vy index 739a3f5d..edba9178 100644 --- a/contracts/main/CurveCryptoViews2Optimized.vy +++ b/contracts/main/CurveCryptoViews2Optimized.vy @@ -43,6 +43,13 @@ interface Math: D: uint256, i: uint256, ) -> uint256[2]: view + def newton_y( + ANN: uint256, + gamma: uint256, + x: uint256[N_COINS], + D: uint256, + i: uint256, + ) -> uint256: view def reduction_coefficient( x: uint256[N_COINS], fee_gamma: uint256 ) -> uint256: view @@ -162,14 +169,11 @@ def _calc_D_ramp( ) -> uint256: math: Math = Curve(swap).MATH() - D: uint256 = Curve(swap).D() if Curve(swap).future_A_gamma_time() > block.timestamp: _xp: uint256[N_COINS] = xp _xp[0] *= precisions[0] - _xp[1] = ( - _xp[1] * price_scale * precisions[1] / PRECISION - ) + _xp[1] = _xp[1] * price_scale * precisions[1] / PRECISION D = math.newton_D(A, gamma, _xp, 0) return D @@ -201,16 +205,15 @@ def _get_dx_fee( # adjust xp with output dy. dy contains fee element, which we handle later # (hence this internal method is called _get_dx_fee) xp[j] -= dy - xp = [xp[0] * precisions[0], xp[1] * price_scale / PRECISION] + xp = [xp[0] * precisions[0], xp[1] * price_scale * precisions[1] / PRECISION] x_out: uint256[2] = math.get_y(A, gamma, xp, D, i) dx: uint256 = x_out[0] - xp[i] xp[i] = x_out[0] if i > 0: - dx = dy * PRECISION / price_scale - else: - dx /= precisions[0] + dx = dy * PRECISION / (price_scale * precisions[1]) + dx /= precisions[i] return dx, xp @@ -238,15 +241,19 @@ def _get_dy_nofee( # adjust xp with input dx xp[i] += dx - xp = [xp[0] * precisions[0], xp[1] * price_scale / PRECISION] + xp = [ + xp[0] * precisions[0], + xp[1] * price_scale * precisions[1] / PRECISION + ] - y_out: uint256[2] = math.get_y(A, gamma, xp, D, j) - dy: uint256 = xp[j] - y_out[0] - 1 - xp[j] = y_out[0] + y_out_newton: uint256 = math.newton_y(A, gamma, xp, D, j) + y_out: uint256 = math.get_y(A, gamma, xp, D, j)[0] + + dy: uint256 = xp[j] - y_out - 1 + xp[j] = y_out if j > 0: dy = dy * PRECISION / price_scale - else: - dy /= precisions[0] + dy /= precisions[j] return dy, xp @@ -281,11 +288,11 @@ def _calc_dtoken_nofee( xp = [ xp[0] * precisions[0], - xp[1] * price_scale / PRECISION + xp[1] * price_scale * precisions[1] / PRECISION ] amountsp = [ amountsp[0] * precisions[0], - amountsp[1] * price_scale / PRECISION + amountsp[1] * price_scale * precisions[1] / PRECISION ] D: uint256 = math.newton_D(A, gamma, xp, 0) @@ -314,7 +321,6 @@ def _calc_withdraw_one_coin( math: Math = Curve(swap).MATH() xx: uint256[N_COINS] = empty(uint256[N_COINS]) - price_scale: uint256 = Curve(swap).price_scale() for k in range(N_COINS): xx[k] = Curve(swap).balances(k) @@ -324,7 +330,7 @@ def _calc_withdraw_one_coin( D0: uint256 = 0 p: uint256 = 0 - price_scale_i: uint256 = PRECISION * precisions[0] + price_scale_i: uint256 = Curve(swap).price_scale() * precisions[1] xp: uint256[N_COINS] = [ xx[0] * precisions[0], unsafe_div(xx[1] * price_scale_i, PRECISION) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index acba0d7b..fa19a417 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -134,7 +134,7 @@ cached_price_scale: uint256 # <------------------------ Internal price scale. cached_price_oracle: uint256 # <------- Price target given by moving average. cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. -last_prices: uint256 +last_prices: public(uint256) last_timestamp: public(uint256[2]) # idx 0 is for prices, idx 1 is for xcp. last_xcp: public(uint256) xcp_ma_time: public(uint256) @@ -152,8 +152,8 @@ future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. balances: public(uint256[N_COINS]) spot_wallet_balances: public(HashMap[address, uint256[N_COINS]]) # <---- Spot -# Wallet is a hashmap that stores balances for users, should they wish -# to not do ERC20 token transfers immediately out of the pool contract. +# Wallet is a hashmap that stores coin balances for users. This cannot +# be commingled with balances. D: public(uint256) xcp_profit: public(uint256) xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees. @@ -546,7 +546,8 @@ def exchange_received_split( and helps reduce the total number of ERC20 tokens per arb transaction to 2: arbitrageur can withdraw profits at a later time and do not need to hedge atomically (which is expensive). - Note for users: please transfer + exchange_received in 1 tx. + Note for users: please transfer + exchange_received_split in 1 tx if + expect_optimistic_transfer is set to True. @param i Index value for the input coin @param j Index value for the output coin @param split_in Amount of input coin being swapped in. @@ -560,15 +561,12 @@ def exchange_received_split( @param split_out Array of output amounts that are handled by the pool. There are two elements in the array: index 0 is the amount of coin[j] sent out to `receiver`. The rest goes into msg.sender's spot wallet balances. - @param receiver Address to send the output coin to - @param use_spot_balance If True, do not do ERC20 token transfers and only use - tokens in user's spot wallet account. + @param receiver Address to send split_out[0] amount of the output coin to @param expect_optimistic_transfer If True: user needs to do a transfer into the pool similar to exchange_received, and then call this method. @return uint256 Amount of tokens at index j received by the `receiver` """ - # _transfer_in updates self.balances here: dx_received: uint256 = 0 if split_in[0] > 0: # Two cases: @@ -659,11 +657,11 @@ def add_liquidity( xp = [ xp[0] * PRECISIONS[0], - xp[1] * price_scale / PRECISION + xp[1] * price_scale * PRECISIONS[1] / PRECISION ] xp_old = [ xp_old[0] * PRECISIONS[0], - xp_old[1] * price_scale / PRECISION + xp_old[1] * price_scale * PRECISIONS[1] / PRECISION ] for i in range(N_COINS): # TODO: optimize @@ -801,7 +799,7 @@ def remove_liquidity( # Update xcp since liquidity was removed: xp: uint256[N_COINS] = self.xp(self.balances, self.cached_price_scale) - self.last_xcp = isqrt(xp[0] * xp[1] / 10**18) + self.last_xcp = isqrt(xp[0] * xp[1]) last_timestamp: uint256[2] = self.last_timestamp if last_timestamp[1] < block.timestamp: @@ -937,8 +935,8 @@ def _exchange( xp: uint256[N_COINS] = self.balances dy: uint256 = 0 - y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert. - x0: uint256 = xp[i] # <--------------- if i > N_COINS, this will revert. + y: uint256 = xp[j] + x0: uint256 = xp[i] xp[i] = x0 + dx_received @@ -1116,7 +1114,7 @@ def tweak_price( if old_virtual_price > 0: - xcp: uint256 = isqrt(xp[0] * xp[1] / 10**18) # TODO: Check precision! + xcp: uint256 = isqrt(xp[0] * xp[1]) virtual_price = 10**18 * xcp / total_supply xcp_profit = unsafe_div( @@ -1186,13 +1184,13 @@ def tweak_price( # ------------------------------------- Convert xp to real prices. xp = [ unsafe_div(D, N_COINS), - D * PRECISION / (N_COINS * p_new) # TODO: can use unsafediv? + D * PRECISION / (N_COINS * p_new) ] # ---------- Calculate new virtual_price using new xp and D. Reuse # `old_virtual_price` (but it has new virtual_price). old_virtual_price = unsafe_div( - 10**18 * isqrt(xp[0] * xp[1] / 10**18), total_supply # TODO: can use unsafemath? + 10**18 * isqrt(xp[0] * xp[1]), total_supply ) # <----- unsafe_div because we did safediv before (if vp>1e18) # ---------------------------- Proceed if we've got enough profit. @@ -1258,7 +1256,8 @@ def _claim_admin_fees(): vprice: uint256 = self.virtual_price price_scale: uint256 = self.cached_price_scale fee_receiver: address = factory.fee_receiver() - balances: uint256[N_COINS] = self.balances + balances: uint256[N_COINS] = self.balances # <- since there's no gulping, + # admin cannot commingle user spot balance with pool balances. # Admin fees are calculated as follows. # 1. Calculate accrued profit since last claim. `xcp_profit` @@ -1397,7 +1396,7 @@ def get_xcp(D: uint256, price_scale: uint256) -> uint256: D * PRECISION / (price_scale * N_COINS) ] - return isqrt(x[0] * x[1] / 10**18) # <------------------- Geometric Mean. # TODO: Check precision! + return isqrt(x[0] * x[1]) # <------------------- Geometric Mean. # TODO: Check precision! @view @@ -1725,7 +1724,7 @@ def lp_price() -> uint256: 0th index @return uint256 LP price. """ - return 2 * self.virtual_price * isqrt(self.cached_price_oracle) / 10**18 # TODO: Check precision. + return 2 * self.virtual_price * isqrt(self.cached_price_oracle) / 10**18 @external diff --git a/tests/fixtures/pool.py b/tests/fixtures/pool.py index 82bbaaa0..b77d7541 100644 --- a/tests/fixtures/pool.py +++ b/tests/fixtures/pool.py @@ -18,7 +18,11 @@ def _get_deposit_amounts(amount_per_token_usd, initial_prices, coins): def _crypto_swap_with_deposit( - coins, user, tricrypto_swap, initial_prices, dollar_amt_each_coin=10**6 + coins, + user, + tricrypto_swap, + initial_prices, + dollar_amt_each_coin=int(1.5 * 10**6), ): # add 1M of each token to the pool @@ -134,16 +138,11 @@ def swap_multiprecision( @pytest.fixture(scope="module") def swap_with_deposit(swap, coins, user): - yield _crypto_swap_with_deposit(coins, user, swap, INITIAL_PRICES) - - -@pytest.fixture(scope="module") -def hyper_swap_with_deposit(hyper_swap, coins, user): - yield _crypto_swap_with_deposit(coins, user, hyper_swap, INITIAL_PRICES) + return _crypto_swap_with_deposit(coins, user, swap, INITIAL_PRICES) @pytest.fixture(scope="module") def yuge_swap(swap, coins, user): - yield _crypto_swap_with_deposit( + return _crypto_swap_with_deposit( coins, user, swap, INITIAL_PRICES, dollar_amt_each_coin=10**10 ) diff --git a/tests/unitary/math/conftest.py b/tests/unitary/math/conftest.py new file mode 100644 index 00000000..35e87c3e --- /dev/null +++ b/tests/unitary/math/conftest.py @@ -0,0 +1,14 @@ +import boa +import pytest + + +@pytest.fixture(scope="module") +def math_optimized(deployer): + with boa.env.prank(deployer): + return boa.load("contracts/main/CurveCryptoMathOptimized2.vy") + + +@pytest.fixture(scope="module") +def math_unoptimized(deployer): + with boa.env.prank(deployer): + return boa.load("contracts/experimental/n=2.vy") diff --git a/tests/unitary/math/misc.py b/tests/unitary/math/misc.py new file mode 100644 index 00000000..2a36a01c --- /dev/null +++ b/tests/unitary/math/misc.py @@ -0,0 +1,39 @@ +from decimal import Decimal + + +def get_y_n2_dec(ANN, gamma, x, D, i): + + if i == 0: + m = 1 + elif i == 1: + m = 0 + + A = Decimal(ANN) / 10**4 / 4 + gamma = Decimal(gamma) / 10**18 + x = [Decimal(_x) / 10**18 for _x in x] + D = Decimal(D) / 10**18 + + a = Decimal(16) * x[m] ** 3 / D**3 + b = 4 * A * gamma**2 * x[m] - (4 * (3 + 2 * gamma) * x[m] ** 2) / D + c = ( + D * (3 + 4 * gamma + (1 - 4 * A) * gamma**2) * x[m] + + 4 * A * gamma**2 * x[m] ** 2 + ) + d = -(Decimal(1) / 4) * D**3 * (1 + gamma) ** 2 + + delta0 = b**2 - 3 * a * c + delta1 = 2 * b**3 - 9 * a * b * c + 27 * a**2 * d + sqrt_arg = delta1**2 - 4 * delta0**3 + + if sqrt_arg < 0: + return [0, {}] + + sqrt = sqrt_arg ** (Decimal(1) / 2) + cbrt_arg = (delta1 + sqrt) / 2 + if cbrt_arg > 0: + C1 = cbrt_arg ** (Decimal(1) / 3) + else: + C1 = -((-cbrt_arg) ** (Decimal(1) / 3)) + root = -(b + C1 + delta0 / C1) / (3 * a) + + return [root, (a, b, c, d)] diff --git a/tests/unitary/math/test_get_y.py b/tests/unitary/math/test_get_y.py new file mode 100644 index 00000000..9e935f5c --- /dev/null +++ b/tests/unitary/math/test_get_y.py @@ -0,0 +1,102 @@ +# flake8: noqa +import time +from decimal import Decimal + +import pytest +from hypothesis import given, note, settings +from hypothesis import strategies as st + +N_COINS = 2 +MAX_SAMPLES = 1000000 # Increase for fuzzing + +A_MUL = 10000 * 2**2 +MIN_A = int(0.01 * A_MUL) +MAX_A = 1000 * A_MUL + +# gamma from 1e-8 up to 0.05 +MIN_GAMMA = 10**10 +MAX_GAMMA = 5 * 10**16 + +pytest.current_case_id = 0 +pytest.negative_sqrt_arg = 0 +pytest.gas_original = 0 +pytest.gas_new = 0 +pytest.t_start = time.time() + + +def inv_target_decimal_n2(A, gamma, x, D): + N = len(x) + + x_prod = Decimal(1) + for x_i in x: + x_prod *= x_i + K0 = x_prod / (Decimal(D) / N) ** N + K0 *= 10**18 + + if gamma > 0: + # K = gamma**2 * K0 / (gamma + 10**18*(Decimal(1) - K0))**2 + K = gamma**2 * K0 / (gamma + 10**18 - K0) ** 2 / 10**18 + K *= A + + f = ( + K * D ** (N - 1) * sum(x) + + x_prod + - (K * D**N + (Decimal(D) / N) ** N) + ) + + return f + + +@given( + A=st.integers(min_value=MIN_A, max_value=MAX_A), + D=st.integers( + min_value=10**18, max_value=10**14 * 10**18 + ), # 1 USD to 100T USD + xD=st.integers( + min_value=int(1.001e16), max_value=int(0.999e20) + ), # <- ratio 1e18 * x/D, typically 1e18 * 1 + yD=st.integers( + min_value=int(1.001e16), max_value=int(0.999e20) + ), # <- ratio 1e18 * y/D, typically 1e18 * 1 + gamma=st.integers(min_value=MIN_GAMMA, max_value=MAX_GAMMA), + j=st.integers(min_value=0, max_value=1), +) +@settings(max_examples=MAX_SAMPLES, deadline=None) +def test_get_y(math_unoptimized, math_optimized, A, D, xD, yD, gamma, j): + pytest.current_case_id += 1 + X = [D * xD // 10**18, D * yD // 10**18] + + A_dec = Decimal(A) / 10000 / 4 + + def calculate_F_by_y0(y0): + new_X = X[:] + new_X[j] = y0 + return inv_target_decimal_n2(A_dec, gamma, new_X, D) + + result_original = math_unoptimized.newton_y(A, gamma, X, D, j) + pytest.gas_original += math_unoptimized._computation.get_gas_used() + + result_get_y, K0 = math_optimized.get_y(A, gamma, X, D, j) + pytest.gas_new += math_optimized._computation.get_gas_used() + + note( + "{" + f"'ANN': {A}, 'GAMMA': {gamma}, 'x': {X}, 'D': {D}, 'index': {j}" + "}\n" + ) + + if K0 == 0: + pytest.negative_sqrt_arg += 1 + return + + if pytest.current_case_id % 1000 == 0: + print( + f"--- {pytest.current_case_id}\nPositive dy frac: {100*pytest.negative_sqrt_arg/pytest.current_case_id:.1f}%\t{time.time() - pytest.t_start:.1f} seconds.\n" + f"Gas advantage per call: {pytest.gas_original//pytest.current_case_id} {pytest.gas_new//pytest.current_case_id}\n" + ) + + assert abs(result_original - result_get_y) <= max( + 10**4, result_original / 1e8 + ) or abs(calculate_F_by_y0(result_get_y)) <= abs( + calculate_F_by_y0(result_original) + ) diff --git a/tests/unitary/math/test_newton_D.py b/tests/unitary/math/test_newton_D.py new file mode 100644 index 00000000..26bf1973 --- /dev/null +++ b/tests/unitary/math/test_newton_D.py @@ -0,0 +1,241 @@ +# flake8: noqa +import sys +import time +from decimal import Decimal + +import pytest +import yaml +from boa.vyper.contract import BoaError +from hypothesis import given, settings +from hypothesis import strategies as st + +import tests.utils.simulation_int_many as sim + +sys.stdout = sys.stderr + + +def inv_target_decimal_n2(A, gamma, x, D): + N = len(x) + + x_prod = Decimal(1) + for x_i in x: + x_prod *= x_i + K0 = x_prod / (Decimal(D) / N) ** N + K0 *= 10**18 + + if gamma > 0: + # K = gamma**2 * K0 / (gamma + 10**18*(Decimal(1) - K0))**2 + K = gamma**2 * K0 / (gamma + 10**18 - K0) ** 2 / 10**18 + K *= A + + f = ( + K * D ** (N - 1) * sum(x) + + x_prod + - (K * D**N + (Decimal(D) / N) ** N) + ) + + return f + + +N_COINS = 2 +MAX_SAMPLES = 3000000 # Increase for fuzzing + +A_MUL = 10000 * 2**2 +MIN_A = int(0.01 * A_MUL) +MAX_A = 1000 * A_MUL + +# gamma from 1e-8 up to 0.05 +MIN_GAMMA = 10**10 +MAX_GAMMA = 5 * 10**16 + +MIN_XD = 10**16 - 1 +MAX_XD = 10**20 + 1 + +pytest.progress = 0 +pytest.positive_dy = 0 +pytest.t_start = time.time() +pytest.gas_original = 0 +pytest.gas_new = 0 +failed_cases = [] + + +@given( + A=st.integers(min_value=MIN_A, max_value=MAX_A), + D=st.integers( + min_value=10**18, max_value=10**14 * 10**18 + ), # 1 USD to 100T USD + xD=st.integers( + min_value=int(1.001e16), max_value=int(0.999e20) + ), # <- ratio 1e18 * x/D, typically 1e18 * 1 + yD=st.integers( + min_value=int(1.001e16), max_value=int(0.999e20) + ), # <- ratio 1e18 * y/D, typically 1e18 * 1 + gamma=st.integers(min_value=MIN_GAMMA, max_value=MAX_GAMMA), + j=st.integers(min_value=0, max_value=1), + btcScalePrice=st.integers(min_value=10**2, max_value=10**7), + ethScalePrice=st.integers(min_value=10, max_value=10**5), + mid_fee=st.sampled_from( + [ + int(0.7e-3 * 10**10), + int(1e-3 * 10**10), + int(1.2e-3 * 10**10), + int(4e-3 * 10**10), + ] + ), + out_fee=st.sampled_from([int(4.0e-3 * 10**10), int(10.0e-3 * 10**10)]), + fee_gamma=st.sampled_from([int(1e-2 * 1e18), int(2e-6 * 1e18)]), +) +@settings(max_examples=MAX_SAMPLES, deadline=None) +def test_newton_D( + math_optimized, + math_unoptimized, + A, + D, + xD, + yD, + gamma, + j, + btcScalePrice, + ethScalePrice, + mid_fee, + out_fee, + fee_gamma, +): + _test_newton_D( + math_optimized, + math_unoptimized, + A, + D, + xD, + yD, + gamma, + j, + btcScalePrice, + ethScalePrice, + mid_fee, + out_fee, + fee_gamma, + ) + + +def _test_newton_D( + math_optimized, + math_unoptimized, + A, + D, + xD, + yD, + gamma, + j, + btcScalePrice, + ethScalePrice, + mid_fee, + out_fee, + fee_gamma, +): + + is_safe = all( + f >= MIN_XD and f <= MAX_XD + for f in [xx * 10**18 // D for xx in [xD, yD]] + ) + + pytest.progress += 1 + if pytest.progress % 1000 == 0 and pytest.positive_dy != 0: + print( + f"{pytest.progress} | {pytest.positive_dy} cases processed in {time.time()-pytest.t_start:.1f} seconds." + f"Gas advantage per call: {pytest.gas_original//pytest.positive_dy} {pytest.gas_new//pytest.positive_dy}\n" + ) + X = [D * xD // 10**18, D * yD // 10**18] + + result_get_y = 0 + get_y_failed = False + try: + (result_get_y, K0) = math_optimized.get_y(A, gamma, X, D, j) + except: + get_y_failed = True + + if get_y_failed: + newton_y_failed = False + try: + math_optimized.internal._newton_y(A, gamma, X, D, j) + except: + newton_y_failed = True + + if get_y_failed and newton_y_failed: + return # both canonical and new method fail, so we ignore. + + if get_y_failed and not newton_y_failed and is_safe: + raise # this is a problem + + # dy should be positive + if result_get_y < X[j]: + + price_scale = (btcScalePrice, ethScalePrice) + y = X[j] + dy = X[j] - result_get_y + dy -= 1 + + if j > 0: + dy = dy * 10**18 // price_scale[j - 1] + + fee = sim.get_fee(X, fee_gamma, mid_fee, out_fee) + dy -= fee * dy // 10**10 + y -= dy + + if dy / X[j] <= 0.95: + + pytest.positive_dy += 1 + X[j] = y + + try: + result_sim = math_unoptimized.newton_D(A, gamma, X) + except: + raise # this is a problem + + try: + result_sim = math_unoptimized.newton_D(A, gamma, X) + pytest.gas_original += ( + math_unoptimized._computation.get_gas_used() + ) + try: + result_contract = math_optimized.newton_D(A, gamma, X, K0) + pytest.gas_new += ( + math_optimized._computation.get_gas_used() + ) + except BoaError as e: + # print(e) + case = ( + "{" + f"'ANN': {A}, 'D': {D}, 'xD': {xD}, 'yD': {yD}, 'GAMMA': {gamma}, 'j': {j}, 'btcScalePrice': {btcScalePrice}, 'ethScalePrice': {ethScalePrice}, 'mid_fee': {mid_fee}, 'out_fee': {out_fee}, 'fee_gamma': {fee_gamma}" + "},\n" + ) + print(case) + raise + + A_dec = Decimal(A) / 10000 / 4 + + def calculate_D_polynome(d): + d = Decimal(d) + return abs(inv_target_decimal_n2(A_dec, gamma, X, d)) + + # print(f"ANN={A}; GAMMA={gamma}; x={X}") + + # D0 = int(2 * (X[0]*X[1])**(Decimal(1)/2)) + # D0_new = int((10**18*4*X[0]*X[1]//K0)**(Decimal(1)/2)) + # print(math_unoptimized._computation.get_gas_used(), D0, calculate_D_polynome(D0)) + # print(math_optimized._computation.get_gas_used(), D0_new, calculate_D_polynome(D0_new)) + + try: + assert abs(result_sim - result_contract) <= max( + 10000, result_sim / 1e12 + ) + except AssertionError: + case = ( + "{" + f"'ANN': {A}, 'D': {D}, 'xD': {xD}, 'yD': {yD}, 'GAMMA': {gamma}, 'j': {j}, 'btcScalePrice': {btcScalePrice}, 'ethScalePrice': {ethScalePrice}, 'mid_fee': {mid_fee}, 'out_fee': {out_fee}, 'fee_gamma': {fee_gamma}" + "},\n" + ) + with open("newton_D_n=2_cases.txt", "a") as f: + f.write(case) + except: + raise diff --git a/tests/unitary/pool/test_deposit_withdraw.py b/tests/unitary/pool/test_deposit_withdraw.py new file mode 100644 index 00000000..687c02e7 --- /dev/null +++ b/tests/unitary/pool/test_deposit_withdraw.py @@ -0,0 +1,378 @@ +import math + +import boa +import pytest +from boa.test import strategy +from hypothesis import given, settings + +from tests.fixtures.pool import INITIAL_PRICES +from tests.utils import simulation_int_many as sim +from tests.utils.tokens import mint_for_testing + +SETTINGS = {"max_examples": 100, "deadline": None} + + +def approx(x1, x2, precision): + return abs(math.log(x1 / x2)) <= precision + + +def assert_string_contains(string, substrings): + assert any(substring in string for substring in substrings) + + +@pytest.fixture(scope="module") +def test_1st_deposit_and_last_withdraw(swap, coins, user, fee_receiver): + + quantities = [10**36 // p for p in INITIAL_PRICES] # $3M worth + + for coin, q in zip(coins, quantities): + mint_for_testing(coin, user, q) + with boa.env.prank(user): + coin.approve(swap, 2**256 - 1) + + bal_before = boa.env.get_balance(swap.address) + with boa.env.prank(user): + swap.add_liquidity(quantities, 0) + + # test if eth wasnt deposited: + assert boa.env.get_balance(swap.address) == bal_before + + token_balance = swap.balanceOf(user) + assert ( + token_balance == swap.totalSupply() - swap.balanceOf(fee_receiver) > 0 + ) + assert abs(swap.get_virtual_price() / 1e18 - 1) < 1e-3 + + # Empty the contract + with boa.env.prank(user): + swap.remove_liquidity(token_balance, [0] * len(coins)) + + # nothing is left except admin balances + assert ( + swap.balanceOf(user) + == swap.totalSupply() - swap.balanceOf(fee_receiver) + == 0 + ) + + return swap + + +@pytest.fixture(scope="module") +def test_first_deposit_full_withdraw_second_deposit( + test_1st_deposit_and_last_withdraw, user, coins, fee_receiver +): + swap = test_1st_deposit_and_last_withdraw + quantities = [10**36 // p for p in INITIAL_PRICES] # $2M worth + + for coin, q in zip(coins, quantities): + mint_for_testing(coin, user, q) + with boa.env.prank(user): + coin.approve(swap, 2**256 - 1) + + # Second deposit + eth_bal_before = boa.env.get_balance(swap.address) + swap_balances_before = [swap.balances(i) for i in range(3)] + with boa.env.prank(user): + swap.add_liquidity(quantities, 0) + + assert swap.xcp_profit_a() >= 10**18 + assert swap.xcp_profit() >= 10**18 + assert swap.virtual_price() >= 10**18 + + # test if eth was not deposited: + assert boa.env.get_balance(swap.address) == eth_bal_before + + for i in range(len(coins)): + assert swap.balances(i) == quantities[i] + swap_balances_before[i] + + token_balance = swap.balanceOf(user) + assert ( + token_balance + swap.balanceOf(fee_receiver) == swap.totalSupply() > 0 + ) + assert abs(swap.get_virtual_price() / 1e18 - 1) < 1e-3 + + return swap + + +@given( + i=strategy("uint", min_value=0, max_value=1), + amount=strategy("uint256", max_value=2000, min_value=1), +) +@settings(**SETTINGS) +def test_second_deposit_single_token( + swap_with_deposit, coins, user, i, amount +): + + # deposit single token: + quantities = [0, 0] + for j in range(len(coins)): + if j == i: + quantities[j] = amount * 10 ** coins[i].decimals() + mint_for_testing(coins[j], user, quantities[j]) + + # Single sided deposit + with boa.env.prank(user): + swap_with_deposit.add_liquidity(quantities, 0) + + +@given( + values=strategy( + "uint256[2]", min_value=10**16, max_value=10**9 * 10**18 + ) +) +@settings(**SETTINGS) +def test_second_deposit( + swap_with_deposit, + coins, + user, + values, +): + + amounts = [v * 10**18 // p for v, p in zip(values, INITIAL_PRICES)] + + # get simmed D value here: + xp = [10**6 * 10**18] * len(coins) # initial D + + # D after second deposit: + for i in range(len(coins)): + xp[i] += int(values[i] * 10**18) + + _A, _gamma = [swap_with_deposit.A(), swap_with_deposit.gamma()] + _D = sim.solve_D(_A, _gamma, xp) + + safe = all( + f >= 1.1e16 and f <= 0.9e20 for f in [_x * 10**18 // _D for _x in xp] + ) + + for coin, q in zip(coins, amounts): + mint_for_testing(coin, user, 10**30) + with boa.env.prank(user): + coin.approve(swap_with_deposit, 2**256 - 1) + + try: + + calculated = swap_with_deposit.calc_token_amount(amounts, True) + measured = swap_with_deposit.balanceOf(user) + d_balances = [swap_with_deposit.balances(i) for i in range(len(coins))] + claimed_fees = [0] * len(coins) + + with boa.env.prank(user): + swap_with_deposit.add_liquidity(amounts, int(calculated * 0.999)) + + logs = swap_with_deposit.get_logs() + for log in logs: + if log.event_type.name == "ClaimAdminFee": + claimed_fees = log.args[0] + + d_balances = [ + swap_with_deposit.balances(i) - d_balances[i] + claimed_fees[i] + for i in range(len(coins)) + ] + measured = swap_with_deposit.balanceOf(user) - measured + + assert calculated <= measured + assert tuple(amounts) == tuple(d_balances) + + except Exception: + + if safe: + raise + + # This is to check that we didn't end up in a borked state after + # a deposit succeeded + swap_with_deposit.get_dy(0, 1, 10**16) + + +@given( + value=strategy( + "uint256", min_value=10**16, max_value=10**6 * 10**18 + ), + i=strategy("uint", min_value=0, max_value=1), +) +@settings(**SETTINGS) +def test_second_deposit_one( + swap_with_deposit, + views_contract, + coins, + user, + value, + i, +): + + amounts = [0] * len(coins) + amounts[i] = value * 10**18 // (INITIAL_PRICES)[i] + mint_for_testing(coins[i], user, amounts[i]) + + try: + + calculated = views_contract.calc_token_amount( + amounts, True, swap_with_deposit + ) + measured = swap_with_deposit.balanceOf(user) + d_balances = [swap_with_deposit.balances(i) for i in range(len(coins))] + claimed_fees = [0] * len(coins) + + with boa.env.prank(user): + swap_with_deposit.add_liquidity(amounts, int(calculated * 0.999)) + + logs = swap_with_deposit.get_logs() + for log in logs: + if log.event_type.name == "ClaimAdminFee": + claimed_fees = log.args[0] + + d_balances = [ + swap_with_deposit.balances(i) - d_balances[i] + claimed_fees[i] + for i in range(len(coins)) + ] + measured = swap_with_deposit.balanceOf(user) - measured + + assert calculated <= measured + assert tuple(amounts) == tuple(d_balances) + + except boa.BoaError as b_error: + + assert_string_contains( + b_error.stack_trace.last_frame.pretty_vm_reason, + ["Unsafe value for y", "Unsafe values x[i]"], + ) + + +@given( + token_amount=strategy( + "uint256", min_value=10**12, max_value=4000 * 10**18 + ) +) # supply is 2400 * 1e18 +@settings(**SETTINGS) +def test_immediate_withdraw( + swap_with_deposit, + views_contract, + coins, + user, + token_amount, +): + + f = token_amount / swap_with_deposit.totalSupply() + if f <= 1: + expected = [ + int(f * swap_with_deposit.balances(i)) for i in range(len(coins)) + ] + measured = [c.balanceOf(user) for c in coins] + token_amount_calc = views_contract.calc_token_amount( + expected, False, swap_with_deposit + ) + assert abs(token_amount_calc - token_amount) / token_amount < 1e-3 + d_balances = [swap_with_deposit.balances(i) for i in range(len(coins))] + + with boa.env.prank(user): + swap_with_deposit.remove_liquidity( + token_amount, + [int(0.999 * e) for e in expected], + ) + + d_balances = [ + d_balances[i] - swap_with_deposit.balances(i) + for i in range(len(coins)) + ] + measured = [c.balanceOf(user) - m for c, m in zip(coins, measured)] + + for e, m in zip(expected, measured): + assert abs(e - m) / e < 1e-3 + + assert tuple(d_balances) == tuple(measured) + + else: + with boa.reverts(), boa.env.prank(user): + swap_with_deposit.remove_liquidity(token_amount, [0] * len(coins)) + + +@given( + token_amount=strategy( + "uint256", min_value=10**12, max_value=4 * 10**6 * 10**18 + ), # Can be more than we have + i=strategy("uint", min_value=0, max_value=1), +) +@settings(**SETTINGS) +def test_immediate_withdraw_one( + swap_with_deposit, + views_contract, + coins, + user, + token_amount, + i, +): + + if token_amount >= swap_with_deposit.totalSupply(): + with boa.reverts(): + swap_with_deposit.calc_withdraw_one_coin(token_amount, i) + + else: + + # Test if we are safe + xp = [10**6 * 10**18] * len(coins) + _supply = swap_with_deposit.totalSupply() + _A, _gamma = [swap_with_deposit.A(), swap_with_deposit.gamma()] + _D = swap_with_deposit.D() * (_supply - token_amount) // _supply + + xp[i] = sim.solve_x(_A, _gamma, xp, _D, i) + + safe = all( + f >= 1.1e16 and f <= 0.9e20 + for f in [_x * 10**18 // _D for _x in xp] + ) + + try: + calculated = swap_with_deposit.calc_withdraw_one_coin( + token_amount, i + ) + except Exception: + if safe: + raise + return + + measured = coins[i].balanceOf(user) + d_balances = [swap_with_deposit.balances(k) for k in range(len(coins))] + claimed_fees = [0] * len(coins) + try: + with boa.env.prank(user): + swap_with_deposit.remove_liquidity_one_coin( + token_amount, i, int(0.999 * calculated) + ) + + logs = swap_with_deposit.get_logs() + for log in logs: + if log.event_type.name == "ClaimAdminFee": + claimed_fees = log.args[0] + + except Exception: + + # Check if it could fall into unsafe region here + frac = ( + (d_balances[i] - calculated) + * (INITIAL_PRICES)[i] + // swap_with_deposit.D() + ) + + if frac > 1.1e16 and frac < 0.9e20: + raise + return # dont continue tests + + d_balances = [ + d_balances[k] - swap_with_deposit.balances(k) + for k in range(len(coins)) + ] + measured = coins[i].balanceOf(user) - measured + + assert calculated >= measured + assert calculated - (0.01 / 100) * calculated < measured + assert approx(calculated, measured, 1e-3) + + for k in range(len(coins)): + claimed_tokens = claimed_fees[k] + if k == i: + assert d_balances[k] == measured + claimed_tokens + else: + assert d_balances[k] == claimed_tokens + + # This is to check that we didn't end up in a borked state after + # a withdrawal succeeded + views_contract.get_dy(0, 1, 10**16, swap_with_deposit) diff --git a/tests/unitary/pool/test_exchange.py b/tests/unitary/pool/test_exchange.py new file mode 100644 index 00000000..d2665df1 --- /dev/null +++ b/tests/unitary/pool/test_exchange.py @@ -0,0 +1,144 @@ +import boa +from boa.test import strategy +from hypothesis import given, settings # noqa + +from tests.fixtures.pool import INITIAL_PRICES +from tests.utils.tokens import mint_for_testing + +SETTINGS = {"max_examples": 100, "deadline": None} + + +@given( + amount=strategy( + "uint256", min_value=10**10, max_value=10**6 * 10**18 + ), # Can be more than we have + i=strategy("uint", min_value=0, max_value=2), + j=strategy("uint", min_value=0, max_value=2), +) +@settings(**SETTINGS) +def test_exchange_all( + swap_with_deposit, + views_contract, + coins, + user, + amount, + i, + j, +): + + if i == j or i > len(coins) - 1 or j > len(coins) - 1: + + with boa.reverts(): + views_contract.get_dy(i, j, 10**6, swap_with_deposit) + + with boa.reverts(), boa.env.prank(user): + swap_with_deposit.exchange(i, j, 10**6, 0) + + else: + + amount = amount * 10**18 // INITIAL_PRICES[i] + mint_for_testing(coins[i], user, amount) + + calculated = views_contract.get_dy(i, j, amount, swap_with_deposit) + + measured_i = coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) + d_balance_i = swap_with_deposit.balances(i) + d_balance_j = swap_with_deposit.balances(j) + + with boa.env.prank(user): + swap_with_deposit.exchange(i, j, amount, int(0.999 * calculated)) + + measured_i -= coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) - measured_j + d_balance_i = swap_with_deposit.balances(i) - d_balance_i + d_balance_j = swap_with_deposit.balances(j) - d_balance_j + + assert amount == measured_i + assert calculated == measured_j + + assert d_balance_i == amount + assert -d_balance_j == measured_j + + +@given( + amount=strategy( + "uint256", min_value=10**10, max_value=10**6 * 10**18 + ), # Can be more than we have + i=strategy("uint", min_value=0, max_value=2), + j=strategy("uint", min_value=0, max_value=2), +) +@settings(**SETTINGS) +def test_exchange_received_success( + swap_with_deposit, + views_contract, + coins, + user, + amount, + i, + j, +): + + if i == j or i > len(coins) - 1 or j > len(coins) - 1: + return + + amount = amount * 10**18 // INITIAL_PRICES[i] + mint_for_testing(coins[i], user, amount) + + calculated = views_contract.get_dy(i, j, amount, swap_with_deposit) + + measured_i = coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) + d_balance_i = swap_with_deposit.balances(i) + d_balance_j = swap_with_deposit.balances(j) + + with boa.env.prank(user): + coins[i].transfer(swap_with_deposit, amount) + swap_with_deposit.exchange_received( + i, j, amount, int(0.999 * calculated), user + ) + + measured_i -= coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) - measured_j + d_balance_i = swap_with_deposit.balances(i) - d_balance_i + d_balance_j = swap_with_deposit.balances(j) - d_balance_j + + assert amount == measured_i + assert calculated == measured_j + + assert d_balance_i == amount + assert -d_balance_j == measured_j + print("pass!") + + +@given( + amount=strategy( + "uint256", min_value=10**10, max_value=10**6 * 10**18 + ), # Can be more than we have + i=strategy("uint", min_value=0, max_value=2), + j=strategy("uint", min_value=0, max_value=2), +) +@settings(**SETTINGS) +def test_exchange_received_revert_on_no_transfer( + swap_with_deposit, + views_contract, + coins, + user, + amount, + i, + j, +): + + if i == j or i > len(coins) or j > len(coins): + + return + + amount = amount * 10**18 // INITIAL_PRICES[i] + mint_for_testing(coins[i], user, amount) + + calculated = views_contract.get_dy(i, j, amount, swap_with_deposit) + + with boa.env.prank(user), boa.reverts(dev="user didn't give us coins"): + swap_with_deposit.exchange_received( + i, j, amount, int(0.999 * calculated), user + ) diff --git a/tests/utils/simulation_int_many.py b/tests/utils/simulation_int_many.py new file mode 100644 index 00000000..57da3bee --- /dev/null +++ b/tests/utils/simulation_int_many.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 +# flake8: noqa +import json + +A_MULTIPLIER = 10000 + + +def geometric_mean(x): + N = len(x) + x = sorted(x, reverse=True) # Presort - good for convergence + D = x[0] + for i in range(255): + D_prev = D + tmp = 10**18 + for _x in x: + tmp = tmp * _x // D + D = D * ((N - 1) * 10**18 + tmp) // (N * 10**18) + diff = abs(D - D_prev) + if diff <= 1 or diff * 10**18 < D: + return D + print(x) + raise ValueError("Did not converge") + + +def reduction_coefficient(x, gamma): + N = len(x) + x_prod = 10**18 + K = 10**18 + S = sum(x) + for x_i in x: + x_prod = x_prod * x_i // 10**18 + K = K * N * x_i // S + if gamma > 0: + K = gamma * 10**18 // (gamma + 10**18 - K) + return K + + +def newton_D(A, gamma, x, D0): + D = D0 + i = 0 + + S = sum(x) + x = sorted(x, reverse=True) + N = len(x) + + for i in range(255): + D_prev = D + + K0 = 10**18 + for _x in x: + K0 = K0 * _x * N // D + + _g1k0 = abs(gamma + 10**18 - K0) + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1 = ( + 10**18 * D // gamma * _g1k0 // gamma * _g1k0 * A_MULTIPLIER // A + ) + + # 2*N*K0 / _g1k0 + mul2 = (2 * 10**18) * N * K0 // _g1k0 + + neg_fprime = ( + (S + S * mul2 // 10**18) + mul1 * N // K0 - mul2 * D // 10**18 + ) + assert neg_fprime > 0 # Python only: -f' > 0 + + # D -= f / fprime + D = (D * neg_fprime + D * S - D**2) // neg_fprime - D * ( + mul1 // neg_fprime + ) // 10**18 * (10**18 - K0) // K0 + + if D < 0: + D = -D // 2 + if abs(D - D_prev) <= max(100, D // 10**14): + return D + + raise ValueError("Did not converge") + + +def newton_y(A, gamma, x, D, i): + N = len(x) + + y = D // N + K0_i = 10**18 + S_i = 0 + x_sorted = sorted(_x for j, _x in enumerate(x) if j != i) + convergence_limit = max(max(x_sorted) // 10**14, D // 10**14, 100) + for _x in x_sorted: + y = y * D // (_x * N) # Small _x first + S_i += _x + for _x in x_sorted[::-1]: + K0_i = K0_i * _x * N // D # Large _x first + + for j in range(255): + y_prev = y + + K0 = K0_i * y * N // D + S = S_i + y + + _g1k0 = abs(gamma + 10**18 - K0) + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1 = ( + 10**18 * D // gamma * _g1k0 // gamma * _g1k0 * A_MULTIPLIER // A + ) + + # 2*K0 / _g1k0 + mul2 = 10**18 + (2 * 10**18) * K0 // _g1k0 + + yfprime = 10**18 * y + S * mul2 + mul1 - D * mul2 + fprime = yfprime // y + assert fprime > 0 # Python only: f' > 0 + + # y -= f / f_prime; y = (y * fprime - f) / fprime + y = ( + yfprime + 10**18 * D - 10**18 * S + ) // fprime + mul1 // fprime * (10**18 - K0) // K0 + + if j > 100: # Just logging when doesn't converge + print(j, y, D, x) + if y < 0 or fprime < 0: + y = y_prev // 2 + if abs(y - y_prev) <= max(convergence_limit, y // 10**14): + return y + + raise Exception("Did not converge") + + +def solve_x(A, gamma, x, D, i): + return newton_y(A, gamma, x, D, i) + + +def solve_D(A, gamma, x): + D0 = len(x) * geometric_mean(x) # <- fuzz to make sure it's ok XXX + return newton_D(A, gamma, x, D0) + + +class Curve: + def __init__(self, A, gamma, D, n, p=None): + self.A = A + self.gamma = gamma + self.n = n + if p: + self.p = p + else: + self.p = [10**18] * n + self.x = [D // n * 10**18 // self.p[i] for i in range(n)] + + def xp(self): + return [x * p // 10**18 for x, p in zip(self.x, self.p)] + + def D(self): + xp = self.xp() + if any(x <= 0 for x in xp): + raise ValueError + return solve_D(self.A, self.gamma, xp) + + def y(self, x, i, j): + xp = self.xp() + xp[i] = x * self.p[i] // 10**18 + yp = solve_x(self.A, self.gamma, xp, self.D(), j) + return yp * 10**18 // self.p[j] + + +def get_data(fname): + with open("download/{0}-1m.json".format(fname), "r") as f: + return [ + { + "open": float(t[1]), + "high": float(t[2]), + "low": float(t[3]), + "close": float(t[4]), + "t": t[0] // 1000, + "volume": float(t[5]), + } + for t in json.load(f) + ] + + +def get_all(): + # 0 - usdt + # 1 - btc + # 2 - eth + out = [] + all_trades = { + name: get_data(name) for name in ["btcusdt", "ethusdt", "ethbtc"] + } + min_time = max(t[0]["t"] for t in all_trades.values()) + max_time = min(t[-1]["t"] for t in all_trades.values()) + for name, pair in [ + ("btcusdt", (0, 1)), + ("ethusdt", (0, 2)), + ("ethbtc", (1, 2)), + ]: + trades = all_trades[name] + for trade in trades: + if trade["t"] >= min_time and trade["t"] <= max_time: + trade["pair"] = pair + out.append((trade["t"] + sum(pair) * 15, trade)) + out = sorted(out) + return [i[1] for i in out] + + +class Trader: + def __init__( + self, + A, + gamma, + D, + n, + p0, + mid_fee=1e-3, + out_fee=3e-3, + allowed_extra_profit=2 * 10**13, + fee_gamma=None, + adjustment_step=0.003, + ma_half_time=500, + log=True, + ): + # allowed_extra_profit is actually not used + self.p0 = p0[:] + self.price_oracle = self.p0[:] + self.last_price = self.p0[:] + self.curve = Curve(A, gamma, D, n, p=p0[:]) + self.dx = int(D * 1e-8) + self.mid_fee = int(mid_fee * 1e10) + self.out_fee = int(out_fee * 1e10) + self.D0 = self.curve.D() + self.xcp_0 = self.get_xcp() + self.xcp_profit = 10**18 + self.xcp_profit_real = 10**18 + self.xcp = self.xcp_0 + self.allowed_extra_profit = allowed_extra_profit + self.adjustment_step = int(10**18 * adjustment_step) + self.log = log + self.fee_gamma = fee_gamma or gamma + self.total_vol = 0.0 + self.ma_half_time = ma_half_time + self.ext_fee = 0 # 0.03e-2 + self.slippage = 0 + self.slippage_count = 0 + + def fee(self): + f = reduction_coefficient(self.curve.xp(), self.fee_gamma) + return (self.mid_fee * f + self.out_fee * (10**18 - f)) // 10**18 + + def price(self, i, j): + dx_raw = self.dx * 10**18 // self.curve.p[i] + return ( + dx_raw + * 10**18 + // (self.curve.x[j] - self.curve.y(self.curve.x[i] + dx_raw, i, j)) + ) + + def step_for_price(self, dp, pair, sign=1): + a, b = pair + p0 = self.price(*pair) + dp = p0 * dp // 10**18 + x0 = self.curve.x[:] + step = self.dx * 10**18 // self.curve.p[a] + while True: + self.curve.x[a] = x0[a] + sign * step + dp_ = abs(p0 - self.price(*pair)) + if dp_ >= dp or step >= self.curve.x[a] // 10: + self.curve.x = x0 + return step + step *= 2 + + def get_xcp(self): + # First calculate the ideal balance + # Then calculate, what the constant-product would be + D = self.curve.D() + N = len(self.curve.x) + X = [D * 10**18 // (N * p) for p in self.curve.p] + + return geometric_mean(X) + + def update_xcp(self, only_real=False): + xcp = self.get_xcp() + self.xcp_profit_real = self.xcp_profit_real * xcp // self.xcp + if not only_real: + self.xcp_profit = self.xcp_profit * xcp // self.xcp + self.xcp = xcp + + def buy(self, dx, i, j, max_price=1e100): + """ + Buy y for x + """ + try: + x_old = self.curve.x[:] + x = self.curve.x[i] + dx + y = self.curve.y(x, i, j) + dy = self.curve.x[j] - y + self.curve.x[i] = x + self.curve.x[j] = y + fee = self.fee() + self.curve.x[j] += dy * fee // 10**10 + dy = dy * (10**10 - fee) // 10**10 + if dx * 10**18 // dy > max_price or dy < 0: + self.curve.x = x_old + return False + self.update_xcp() + return dy + except ValueError: + return False + + def sell(self, dy, i, j, min_price=0): + """ + Sell y for x + """ + try: + x_old = self.curve.x[:] + y = self.curve.x[j] + dy + x = self.curve.y(y, j, i) + dx = self.curve.x[i] - x + self.curve.x[i] = x + self.curve.x[j] = y + fee = self.fee() + self.curve.x[i] += dx * fee // 10**10 + dx = dx * (10**10 - fee) // 10**10 + if dx * 10**18 // dy < min_price or dx < 0: + self.curve.x = x_old + return False + self.update_xcp() + return dx + except ValueError: + return False + + def ma_recorder(self, t, price_vector): + # XXX what if every block only has p_b being last + N = len(price_vector) + if t > self.t: + alpha = 0.5 ** ((t - self.t) / self.ma_half_time) + for k in range(1, N): + self.price_oracle[k] = int( + price_vector[k] * (1 - alpha) + + self.price_oracle[k] * alpha + ) + self.t = t + + def tweak_price(self, t, a, b, p): + self.ma_recorder(t, self.last_price) + if b > 0: + self.last_price[b] = p * self.last_price[a] // 10**18 + else: + self.last_price[a] = self.last_price[0] * 10**18 // p + + # price_oracle looks like [1, p1, p2, ...] normalized to 1e18 + norm = int( + sum( + (p_real * 10**18 // p_target - 10**18) ** 2 + for p_real, p_target in zip(self.price_oracle, self.curve.p) + ) + ** 0.5 + ) + adjustment_step = max(self.adjustment_step, norm // 5) + if norm <= adjustment_step: + # Already close to the target price + return norm + + p_new = [10**18] + p_new += [ + p_target + adjustment_step * (p_real - p_target) // norm + for p_real, p_target in zip( + self.price_oracle[1:], self.curve.p[1:] + ) + ] + + old_p = self.curve.p[:] + old_profit = self.xcp_profit_real + old_xcp = self.xcp + + self.curve.p = p_new + self.update_xcp(only_real=True) + + if 2 * (self.xcp_profit_real - 10**18) <= self.xcp_profit - 10**18: + # If real profit is less than half of maximum - revert params back + self.curve.p = old_p + self.xcp_profit_real = old_profit + self.xcp = old_xcp + + return norm + + def simulate(self, mdata): + lasts = {} + self.t = mdata[0]["t"] + for i, d in enumerate(mdata): + a, b = d["pair"] + vol = 0 + ext_vol = int( + d["volume"] * self.price_oracle[b] + ) # <- now all is in USD + ctr = 0 + last = lasts.get( + (a, b), self.price_oracle[b] * 10**18 // self.price_oracle[a] + ) + _high = last + _low = last + + # Dynamic step + # f = reduction_coefficient(self.curve.xp(), self.curve.gamma) + candle = min( + int(1e18 * abs((d["high"] - d["low"]) / d["high"])), 10**17 + ) + candle = max(10**15, candle) + step1 = self.step_for_price(candle // 50, (a, b), sign=1) + step2 = self.step_for_price(candle // 50, (a, b), sign=-1) + step = min(step1, step2) + + max_price = int(1e18 * d["high"]) + _dx = 0 + p_before = self.price(a, b) + while last < max_price and vol < ext_vol // 2: + dy = self.buy(step, a, b, max_price=max_price) + if dy is False: + break + vol += dy * self.price_oracle[b] // 10**18 + _dx += dy + last = step * 10**18 // dy + max_price = int(1e18 * d["high"]) + ctr += 1 + p_after = self.price(a, b) + if p_before != p_after: + self.slippage_count += 1 + self.slippage += ( + _dx + * self.curve.p[b] + // 10**18 + * (p_before + p_after) + // (2 * abs(p_before - p_after)) + ) + _high = last + min_price = int(1e18 * d["low"]) + _dx = 0 + p_before = p_after + while last > min_price and vol < ext_vol // 2: + dx = step * 10**18 // last + dy = self.sell(dx, a, b, min_price=min_price) + _dx += dx + if dy is False: + break + vol += dx * self.price_oracle[b] // 10**18 + last = dy * 10**18 // dx + min_price = int(10**18 * d["low"]) + ctr += 1 + p_after = self.price(a, b) + if p_before != p_after: + self.slippage_count += 1 + self.slippage += ( + _dx + * self.curve.p[b] + // 10**18 + * (p_before + p_after) + // (2 * abs(p_before - p_after)) + ) + _low = last + lasts[(a, b)] = last + + self.tweak_price(d["t"], a, b, (_high + _low) // 2) + + self.total_vol += vol + if self.log: + try: + print( + ( + """{0:.1f}%\ttrades: {1}\t""" + """AMM: {2:.0f}, {3:.0f}\tTarget: {4:.0f}, {5:.0f}\t""" + """Vol: {6:.4f}\tPR:{7:.2f}\txCP-growth: {8:.5f}\t""" + """APY:{9:.1f}%\tfee:{10:.3f}%""" + ).format( + 100 * i / len(mdata), + ctr, + lasts.get( + (0, 1), + self.price_oracle[1] + * 10**18 + // self.price_oracle[0], + ) + / 1e18, + lasts.get( + (0, 2), + self.price_oracle[2] + * 10**18 + // self.price_oracle[0], + ) + / 1e18, + self.curve.p[1] / 1e18, + self.curve.p[2] / 1e18, + self.total_vol / 1e18, + (self.xcp_profit_real - 10**18) + / (self.xcp_profit - 10**18), + self.xcp_profit_real / 1e18, + ( + (self.xcp_profit_real / 1e18) + ** (86400 * 365 / (d["t"] - mdata[0]["t"] + 1)) + - 1 + ) + * 100, + self.fee() / 1e10 * 100, + ) + ) + except Exception: + pass + + +def get_price_vector(n, data): + p = [10**18] + [None] * (n - 1) + for d in data: + if d["pair"][0] == 0: + p[d["pair"][1]] = int(d["close"] * 1e18) + if all(x is not None for x in p): + return p + + +if __name__ == "__main__": + test_data = get_all()[-100000:] + + trader = Trader( + 135 * 3**3 * 10000, + int(7e-5 * 1e18), + 5_000_000 * 10**18, + 3, + get_price_vector(3, test_data), + mid_fee=4e-4, + out_fee=4.0e-3, + allowed_extra_profit=2 * 10**13, + fee_gamma=int(0.01 * 1e18), + adjustment_step=0.0015, + ma_half_time=600, + ) + + trader.simulate(test_data) From 58ff1780e255ec6c9bafb19a1bc4725ddaefb2f3 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:19:39 +0200 Subject: [PATCH 06/72] fix extra accounting of dx received --- contracts/main/CurveCryptoMathOptimized2.vy | 125 ++++++++++++------- contracts/main/CurveCryptoViews2Optimized.vy | 7 +- contracts/main/CurveTwocryptoOptimized.vy | 13 +- tests/unitary/pool/test_exchange.py | 84 +++++++------ 4 files changed, 130 insertions(+), 99 deletions(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index 1eef9a35..dcb98c57 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -30,18 +30,60 @@ version: public(constant(String[8])) = "v2.0.0" @internal @pure -def _cbrt(x: uint256) -> uint256: +def _snekmate_log_2(x: uint256, roundup: bool) -> uint256: + """ + @notice An `internal` helper function that returns the log in base 2 + of `x`, following the selected rounding direction. + @dev This implementation is derived from Snekmate, which is authored + by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license. + https://github.com/pcaversaccio/snekmate + @dev Note that it returns 0 if given 0. The implementation is + inspired by OpenZeppelin's implementation here: + https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol. + @param x The 32-byte variable. + @param roundup The Boolean variable that specifies whether + to round up or not. The default `False` is round down. + @return uint256 The 32-byte calculation result. + """ + value: uint256 = x + result: uint256 = empty(uint256) + + # The following lines cannot overflow because we have the well-known + # decay behaviour of `log_2(max_value(uint256)) < max_value(uint256)`. + if x >> 128 != empty(uint256): + value = x >> 128 + result = 128 + if value >> 64 != empty(uint256): + value = value >> 64 + result = unsafe_add(result, 64) + if value >> 32 != empty(uint256): + value = value >> 32 + result = unsafe_add(result, 32) + if value >> 16 != empty(uint256): + value = value >> 16 + result = unsafe_add(result, 16) + if value >> 8 != empty(uint256): + value = value >> 8 + result = unsafe_add(result, 8) + if value >> 4 != empty(uint256): + value = value >> 4 + result = unsafe_add(result, 4) + if value >> 2 != empty(uint256): + value = value >> 2 + result = unsafe_add(result, 2) + if value >> 1 != empty(uint256): + result = unsafe_add(result, 1) + + if (roundup and (1 << result) < x): + result = unsafe_add(result, 1) + + return result - # we artificially set a cap to the values for which we can compute the - # cube roots safely. This is not to say that there are no values above - # max(uint256) // 10**36 for which we cannot get good cube root estimates. - # However, beyond this point, accuracy is not guaranteed since overflows - # start to occur. - assert x < 115792089237316195423570985008687907853269, "inaccurate cbrt" +@internal +@pure +def _cbrt(x: uint256) -> uint256: - # we increase precision of input `x` by multiplying 10 ** 36. - # in such cases: cbrt(10**18) = 10**18, cbrt(1) = 10**12 xx: uint256 = 0 if x >= 115792089237316195423570985008687907853269 * 10**18: xx = x @@ -50,45 +92,34 @@ def _cbrt(x: uint256) -> uint256: else: xx = unsafe_mul(x, 10**36) - # get log2(x) for approximating initial value - # logic is: cbrt(a) = cbrt(2**(log2(a))) = 2**(log2(a) / 3) ≈ 2**|log2(a)/3| - # from: https://github.com/transmissions11/solmate/blob/b9d69da49bbbfd090f1a73a4dba28aa2d5ee199f/src/utils/FixedPointMathLib.sol#L352 - - a_pow: int256 = 0 - if xx > 340282366920938463463374607431768211455: - a_pow = 128 - if unsafe_div(xx, shift(2, a_pow)) > 18446744073709551615: - a_pow = a_pow | 64 - if unsafe_div(xx, shift(2, a_pow)) > 4294967295: - a_pow = a_pow | 32 - if unsafe_div(xx, shift(2, a_pow)) > 65535: - a_pow = a_pow | 16 - if unsafe_div(xx, shift(2, a_pow)) > 255: - a_pow = a_pow | 8 - if unsafe_div(xx, shift(2, a_pow)) > 15: - a_pow = a_pow | 4 - if unsafe_div(xx, shift(2, a_pow)) > 3: - a_pow = a_pow | 2 - if unsafe_div(xx, shift(2, a_pow)) > 1: - a_pow = a_pow | 1 - - # initial value: 2**|log2(a)/3| - # which is: 2 ** (n / 3) * 1260 ** (n % 3) / 1000 ** (n % 3) - a_pow_mod: uint256 = convert(a_pow, uint256) % 3 + log2x: int256 = convert(self._snekmate_log_2(xx, False), int256) + + # When we divide log2x by 3, the remainder is (log2x % 3). + # So if we just multiply 2**(log2x/3) and discard the remainder to calculate our + # guess, the newton method will need more iterations to converge to a solution, + # since it is missing that precision. It's a few more calculations now to do less + # calculations later: + # pow = log2(x) // 3 + # remainder = log2(x) % 3 + # initial_guess = 2 ** pow * cbrt(2) ** remainder + # substituting -> 2 = 1.26 ≈ 1260 / 1000, we get: + # + # initial_guess = 2 ** pow * 1260 ** remainder // 1000 ** remainder + + remainder: uint256 = convert(log2x, uint256) % 3 a: uint256 = unsafe_div( unsafe_mul( - pow_mod256( - 2, - unsafe_div( - convert(a_pow, uint256), 3 - ) - ), - pow_mod256(1260, a_pow_mod) + pow_mod256(2, unsafe_div(convert(log2x, uint256), 3)), # <- pow + pow_mod256(1260, remainder), ), - pow_mod256(1000, a_pow_mod) + pow_mod256(1000, remainder), ) - # 7 newton raphson iterations: + # Because we chose good initial values for cube roots, 7 newton raphson iterations + # are just about sufficient. 6 iterations would result in non-convergences, and 8 + # would be one too many iterations. Without initial values, the iteration count + # can go up to 20 or greater. The iterations are unrolled. This reduces gas costs + # but takes up more bytecode: a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) @@ -98,11 +129,11 @@ def _cbrt(x: uint256) -> uint256: a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) if x >= 115792089237316195423570985008687907853269 * 10**18: - return a*10**12 + a = unsafe_mul(a, 10**12) elif x >= 115792089237316195423570985008687907853269: - return a*10**6 - else: - return a + a = unsafe_mul(a, 10**6) + + return a @internal diff --git a/contracts/main/CurveCryptoViews2Optimized.vy b/contracts/main/CurveCryptoViews2Optimized.vy index edba9178..87731dd4 100644 --- a/contracts/main/CurveCryptoViews2Optimized.vy +++ b/contracts/main/CurveCryptoViews2Optimized.vy @@ -246,11 +246,10 @@ def _get_dy_nofee( xp[1] * price_scale * precisions[1] / PRECISION ] - y_out_newton: uint256 = math.newton_y(A, gamma, xp, D, j) - y_out: uint256 = math.get_y(A, gamma, xp, D, j)[0] + y_out: uint256[2] = math.get_y(A, gamma, xp, D, j) - dy: uint256 = xp[j] - y_out - 1 - xp[j] = y_out + dy: uint256 = xp[j] - y_out[0] - 1 + xp[j] = y_out[0] if j > 0: dy = dy * PRECISION / price_scale dy /= precisions[j] diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index fa19a417..a26099ab 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -938,10 +938,7 @@ def _exchange( y: uint256 = xp[j] x0: uint256 = xp[i] - xp[i] = x0 + dx_received - price_scale: uint256 = self.cached_price_scale - xp = [ xp[0] * PRECISIONS[0], unsafe_div(xp[1] * price_scale * PRECISIONS[1], PRECISION) @@ -967,18 +964,16 @@ def _exchange( D: uint256 = self.D y_out: uint256[2] = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, j) dy = xp[j] - y_out[0] - xp[j] = xp[j] - dy - dy = dy - 1 + xp[j] -= dy + dy -= 1 if j > 0: dy = dy * PRECISION / price_scale - dy = dy / PRECISIONS[j] + dy /= PRECISIONS[j] fee: uint256 = unsafe_div(self._fee(xp) * dy, 10**10) - dy -= fee # <--------------------- Subtract fee from the outgoing amount. assert dy >= min_dy, "Slippage" - y -= dy y *= PRECISIONS[j] @@ -1396,7 +1391,7 @@ def get_xcp(D: uint256, price_scale: uint256) -> uint256: D * PRECISION / (price_scale * N_COINS) ] - return isqrt(x[0] * x[1]) # <------------------- Geometric Mean. # TODO: Check precision! + return isqrt(x[0] * x[1]) # <------------------- Geometric Mean. @view diff --git a/tests/unitary/pool/test_exchange.py b/tests/unitary/pool/test_exchange.py index d2665df1..058589a3 100644 --- a/tests/unitary/pool/test_exchange.py +++ b/tests/unitary/pool/test_exchange.py @@ -8,12 +8,27 @@ SETTINGS = {"max_examples": 100, "deadline": None} +def test_exchange_reverts(user, views_contract, swap_with_deposit): + + with boa.reverts(): + views_contract.get_dy(0, 2, 10**6, swap_with_deposit) + + with boa.reverts(): + views_contract.get_dy(2, 1, 10**6, swap_with_deposit) + + with boa.reverts(): + swap_with_deposit.exchange(1, 3, 10**6, 0, sender=user) + + with boa.reverts(): + swap_with_deposit.exchange(2, 2, 10**6, 0, sender=user) + + @given( amount=strategy( "uint256", min_value=10**10, max_value=10**6 * 10**18 ), # Can be more than we have - i=strategy("uint", min_value=0, max_value=2), - j=strategy("uint", min_value=0, max_value=2), + i=strategy("uint", min_value=0, max_value=1), + j=strategy("uint", min_value=0, max_value=1), ) @settings(**SETTINGS) def test_exchange_all( @@ -26,47 +41,40 @@ def test_exchange_all( j, ): - if i == j or i > len(coins) - 1 or j > len(coins) - 1: - - with boa.reverts(): - views_contract.get_dy(i, j, 10**6, swap_with_deposit) - - with boa.reverts(), boa.env.prank(user): - swap_with_deposit.exchange(i, j, 10**6, 0) - - else: + if i == j: + return - amount = amount * 10**18 // INITIAL_PRICES[i] - mint_for_testing(coins[i], user, amount) + amount = amount * 10**18 // INITIAL_PRICES[i] + mint_for_testing(coins[i], user, amount) - calculated = views_contract.get_dy(i, j, amount, swap_with_deposit) + calculated = views_contract.get_dy(i, j, amount, swap_with_deposit) - measured_i = coins[i].balanceOf(user) - measured_j = coins[j].balanceOf(user) - d_balance_i = swap_with_deposit.balances(i) - d_balance_j = swap_with_deposit.balances(j) + measured_i = coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) + d_balance_i = swap_with_deposit.balances(i) + d_balance_j = swap_with_deposit.balances(j) - with boa.env.prank(user): - swap_with_deposit.exchange(i, j, amount, int(0.999 * calculated)) + with boa.env.prank(user): + swap_with_deposit.exchange(i, j, amount, int(0.999 * calculated)) - measured_i -= coins[i].balanceOf(user) - measured_j = coins[j].balanceOf(user) - measured_j - d_balance_i = swap_with_deposit.balances(i) - d_balance_i - d_balance_j = swap_with_deposit.balances(j) - d_balance_j + measured_i -= coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) - measured_j + d_balance_i = swap_with_deposit.balances(i) - d_balance_i + d_balance_j = swap_with_deposit.balances(j) - d_balance_j - assert amount == measured_i - assert calculated == measured_j + assert amount == measured_i + assert calculated == measured_j - assert d_balance_i == amount - assert -d_balance_j == measured_j + assert d_balance_i == amount + assert -d_balance_j == measured_j @given( amount=strategy( "uint256", min_value=10**10, max_value=10**6 * 10**18 ), # Can be more than we have - i=strategy("uint", min_value=0, max_value=2), - j=strategy("uint", min_value=0, max_value=2), + i=strategy("uint", min_value=0, max_value=1), + j=strategy("uint", min_value=0, max_value=1), ) @settings(**SETTINGS) def test_exchange_received_success( @@ -79,7 +87,7 @@ def test_exchange_received_success( j, ): - if i == j or i > len(coins) - 1 or j > len(coins) - 1: + if i == j: return amount = amount * 10**18 // INITIAL_PRICES[i] @@ -94,7 +102,7 @@ def test_exchange_received_success( with boa.env.prank(user): coins[i].transfer(swap_with_deposit, amount) - swap_with_deposit.exchange_received( + out = swap_with_deposit.exchange_received( i, j, amount, int(0.999 * calculated), user ) @@ -104,19 +112,18 @@ def test_exchange_received_success( d_balance_j = swap_with_deposit.balances(j) - d_balance_j assert amount == measured_i - assert calculated == measured_j + assert calculated == measured_j == out assert d_balance_i == amount - assert -d_balance_j == measured_j - print("pass!") + assert -d_balance_j == measured_j == out @given( amount=strategy( "uint256", min_value=10**10, max_value=10**6 * 10**18 ), # Can be more than we have - i=strategy("uint", min_value=0, max_value=2), - j=strategy("uint", min_value=0, max_value=2), + i=strategy("uint", min_value=0, max_value=1), + j=strategy("uint", min_value=0, max_value=1), ) @settings(**SETTINGS) def test_exchange_received_revert_on_no_transfer( @@ -129,8 +136,7 @@ def test_exchange_received_revert_on_no_transfer( j, ): - if i == j or i > len(coins) or j > len(coins): - + if i == j: return amount = amount * 10**18 // INITIAL_PRICES[i] From d879e4c863474dbdbfcdd8177105ec0ffc950002 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:43:54 +0200 Subject: [PATCH 07/72] add cicd --- .github/workflows/.pre-commit-config.yaml | 11 +++++++++ .github/workflows/unit-tests.yaml | 30 +++++++++++++++++++++++ contracts/main/CurveTwocryptoOptimized.vy | 2 +- 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/.pre-commit-config.yaml create mode 100644 .github/workflows/unit-tests.yaml diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml new file mode 100644 index 00000000..81bd81e9 --- /dev/null +++ b/.github/workflows/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +name: pre-commit + +on: [pull_request, push] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v4 + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml new file mode 100644 index 00000000..9bcecfc5 --- /dev/null +++ b/.github/workflows/unit-tests.yaml @@ -0,0 +1,30 @@ +name: unit-tests-boa + +on: ["push", "pull_request"] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + boa-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Cache Compiler Installations + uses: actions/cache@v2 + with: + path: | + ~/.vvm + key: compiler-cache + + - name: Setup Python 3.10.4 + uses: actions/setup-python@v2 + with: + python-version: 3.10.4 + + - name: Install Requirements + run: pip install -r requirements.txt + + - name: Run Tests + run: python -m pytest tests/boa -n auto diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index a26099ab..e62ae376 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -664,7 +664,7 @@ def add_liquidity( xp_old[1] * price_scale * PRECISIONS[1] / PRECISION ] - for i in range(N_COINS): # TODO: optimize + for i in range(N_COINS): if amounts_received[i] > 0: amountsp[i] = xp[i] - xp_old[i] From ca4317cbd1e0a1f9a8d3bb96acd1b855e0f380c9 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:45:48 +0200 Subject: [PATCH 08/72] update boa --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b64f89be..919d198b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,5 @@ pdbpp hypothesis>=6.68.1 # vyper and dev framework: -git+https://github.com/vyperlang/titanoboa@40f5bfcc2afe212bb5a6f5026148f3625596ded7 +git+https://github.com/vyperlang/titanoboa@8485787c5196718c679f82e7ac69fef585a4ce1d vyper>=0.3.10 From 7797ad4bc8ab4a59bad30f5890dbc6ca9ecb6102 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:51:45 +0200 Subject: [PATCH 09/72] fix path --- .github/workflows/unit-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 9bcecfc5..fefed558 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -27,4 +27,4 @@ jobs: run: pip install -r requirements.txt - name: Run Tests - run: python -m pytest tests/boa -n auto + run: python -m pytest tests/ -n auto From 39963c84242d151dd1268e81871201da264c1f6c Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:42:38 +0200 Subject: [PATCH 10/72] update limits in tests --- contracts/main/CurveCryptoMathOptimized2.vy | 1 + tests/unitary/math/test_get_y.py | 6 ++-- tests/unitary/math/test_newton_D.py | 6 ++-- tests/unitary/pool/test_exchange_received.py | 36 ++++++++++++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 tests/unitary/pool/test_exchange_received.py diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index dcb98c57..2b3b0b55 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -230,6 +230,7 @@ def get_y( ) -> uint256[2]: # Safety checks + print(_ANN, MIN_A, MAX_A) assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D diff --git a/tests/unitary/math/test_get_y.py b/tests/unitary/math/test_get_y.py index 9e935f5c..905ab2b0 100644 --- a/tests/unitary/math/test_get_y.py +++ b/tests/unitary/math/test_get_y.py @@ -9,9 +9,9 @@ N_COINS = 2 MAX_SAMPLES = 1000000 # Increase for fuzzing -A_MUL = 10000 * 2**2 -MIN_A = int(0.01 * A_MUL) -MAX_A = 1000 * A_MUL +A_MUL = 10000 +MIN_A = int(N_COINS**N_COINS * A_MUL / 10) +MAX_A = int(N_COINS**N_COINS * A_MUL * 100000) # gamma from 1e-8 up to 0.05 MIN_GAMMA = 10**10 diff --git a/tests/unitary/math/test_newton_D.py b/tests/unitary/math/test_newton_D.py index 26bf1973..d7f15c5e 100644 --- a/tests/unitary/math/test_newton_D.py +++ b/tests/unitary/math/test_newton_D.py @@ -40,9 +40,9 @@ def inv_target_decimal_n2(A, gamma, x, D): N_COINS = 2 MAX_SAMPLES = 3000000 # Increase for fuzzing -A_MUL = 10000 * 2**2 -MIN_A = int(0.01 * A_MUL) -MAX_A = 1000 * A_MUL +A_MUL = 10000 +MIN_A = int(N_COINS**N_COINS * A_MUL / 10) +MAX_A = int(N_COINS**N_COINS * A_MUL * 100000) # gamma from 1e-8 up to 0.05 MIN_GAMMA = 10**10 diff --git a/tests/unitary/pool/test_exchange_received.py b/tests/unitary/pool/test_exchange_received.py new file mode 100644 index 00000000..a1eda8dd --- /dev/null +++ b/tests/unitary/pool/test_exchange_received.py @@ -0,0 +1,36 @@ +import boa +from boa.test import strategy +from hypothesis import given, settings # noqa + +from tests.fixtures.pool import INITIAL_PRICES +from tests.utils.tokens import mint_for_testing + +SETTINGS = {"max_examples": 100, "deadline": None} + + +@given( + amount=strategy( + "uint256", min_value=10**10, max_value=10**6 * 10**18 + ), + split_in=strategy( + "uint256", min_value=0, max_value=100 + ), + split_out=strategy( + "uint256", min_value=0, max_value=100 + ), + i=strategy("uint", min_value=0, max_value=1), + j=strategy("uint", min_value=0, max_value=1), +) +@settings(**SETTINGS) +def test_exchange_split( + swap_with_deposit, + views_contract, + coins, + user, + amount, + split_in, + split_out, + i, + j +): + pass \ No newline at end of file From d2e7fec2d8cdbb773cfc1205478518d320d4f0df Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:43:33 +0200 Subject: [PATCH 11/72] remove print; ignore flake8 --- contracts/main/CurveCryptoMathOptimized2.vy | 1 - tests/unitary/pool/test_exchange_received.py | 13 +++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index 2b3b0b55..dcb98c57 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -230,7 +230,6 @@ def get_y( ) -> uint256[2]: # Safety checks - print(_ANN, MIN_A, MAX_A) assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D diff --git a/tests/unitary/pool/test_exchange_received.py b/tests/unitary/pool/test_exchange_received.py index a1eda8dd..ab54e852 100644 --- a/tests/unitary/pool/test_exchange_received.py +++ b/tests/unitary/pool/test_exchange_received.py @@ -1,3 +1,4 @@ +# flake8: noqa import boa from boa.test import strategy from hypothesis import given, settings # noqa @@ -12,12 +13,8 @@ amount=strategy( "uint256", min_value=10**10, max_value=10**6 * 10**18 ), - split_in=strategy( - "uint256", min_value=0, max_value=100 - ), - split_out=strategy( - "uint256", min_value=0, max_value=100 - ), + split_in=strategy("uint256", min_value=0, max_value=100), + split_out=strategy("uint256", min_value=0, max_value=100), i=strategy("uint", min_value=0, max_value=1), j=strategy("uint", min_value=0, max_value=1), ) @@ -31,6 +28,6 @@ def test_exchange_split( split_in, split_out, i, - j + j, ): - pass \ No newline at end of file + pass From aea8d70e8ffc6cb341e7ebc5d5b31208fb8ce438 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:12:59 +0200 Subject: [PATCH 12/72] add fuzz --- tests/unitary/math/fuzz_multicoin_curve.py | 164 +++++++++++++++++++++ tests/unitary/math/test_newton_D.py | 1 - tests/utils/simulation_int_many.py | 9 +- 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 tests/unitary/math/fuzz_multicoin_curve.py diff --git a/tests/unitary/math/fuzz_multicoin_curve.py b/tests/unitary/math/fuzz_multicoin_curve.py new file mode 100644 index 00000000..fe75c118 --- /dev/null +++ b/tests/unitary/math/fuzz_multicoin_curve.py @@ -0,0 +1,164 @@ +# flake8: noqa +import unittest +from itertools import permutations + +import hypothesis.strategies as st +from hypothesis import given, settings + +from tests.utils.simulation_int_many import ( + Curve, + geometric_mean, + reduction_coefficient, + solve_D, + solve_x, +) + +MAX_EXAMPLES_MEAN = 20000 +MAX_EXAMPLES_RED = 20000 +MAX_EXAMPLES_D = 10000 +MAX_EXAMPLES_Y = 5000 +MAX_EXAMPLES_YD = 100000 +MAX_EXAMPLES_NOLOSS = 100000 +MIN_FEE = 5e-5 + +MIN_XD = 10**16 +MAX_XD = 10**20 + +N_COINS = 2 +A_MUL = 10000 +MIN_A = int(N_COINS**N_COINS * A_MUL / 10) +MAX_A = int(N_COINS**N_COINS * A_MUL * 100000) + +# gamma from 1e-8 up to 0.05 +MIN_GAMMA = 10**10 +MAX_GAMMA = 2 * 10**16 + + +# Test with 2 coins +class TestCurve(unittest.TestCase): + @given( + x=st.integers(10**9, 10**15 * 10**18), + y=st.integers(10**9, 10**15 * 10**18), + ) + @settings(max_examples=MAX_EXAMPLES_MEAN) + def test_geometric_mean(self, x, y): + val = geometric_mean([x, y]) + assert val > 0 + diff = abs((x * y) ** (1 / 2) - val) + assert diff / val <= max(1e-10, 1 / min([x, y])) + + @given( + x=st.integers(10**9, 10**15 * 10**18), + y=st.integers(10**9, 10**15 * 10**18), + gamma=st.integers(10**10, 10**18), + ) + @settings(max_examples=MAX_EXAMPLES_RED) + def test_reduction_coefficient(self, x, y, gamma): + coeff = reduction_coefficient([x, y], gamma) + assert coeff <= 10**18 + + K = 2**2 * x * y / (x + y) ** 2 + if gamma > 0: + K = (gamma / 1e18) / ((gamma / 1e18) + 1 - K) + assert abs(coeff / 1e18 - K) <= 1e-7 + + @given( + A=st.integers(MIN_A, MAX_A), + x=st.integers(10**18, 10**15 * 10**18), # 1 USD to 1e15 USD + yx=st.integers( + 10**14, 10**18 + ), # <- ratio 1e18 * y/x, typically 1e18 * 1 + perm=st.integers(0, 1), # <- permutation mapping to values + gamma=st.integers(MIN_GAMMA, MAX_GAMMA), + ) + @settings(max_examples=MAX_EXAMPLES_D) + def test_D_convergence(self, A, x, yx, perm, gamma): + # Price not needed for convergence testing + pmap = list(permutations(range(2))) + + y = x * yx // 10**18 + curve = Curve(A, gamma, 10**18, 2) + curve.x = [0] * 2 + i, j = pmap[perm] + curve.x[i] = x + curve.x[j] = y + assert curve.D() > 0 + + @given( + A=st.integers(MIN_A, MAX_A), + x=st.integers(10**17, 10**15 * 10**18), # $0.1 .. $1e15 + yx=st.integers(10**15, 10**21), + gamma=st.integers(MIN_GAMMA, MAX_GAMMA), + i=st.integers(0, 1), + inx=st.integers(10**15, 10**21), + ) + @settings(max_examples=MAX_EXAMPLES_Y) + def test_y_convergence(self, A, x, yx, gamma, i, inx): + j = 1 - i + in_amount = x * inx // 10**18 + y = x * yx // 10**18 + curve = Curve(A, gamma, 10**18, 2) + curve.x = [x, y] + out_amount = curve.y(in_amount, i, j) + assert out_amount > 0 + + @given( + A=st.integers(MIN_A, MAX_A), + x=st.integers(10**17, 10**15 * 10**18), # 0.1 USD to 1e15 USD + yx=st.integers(5 * 10**14, 20 * 10**20), + gamma=st.integers(MIN_GAMMA, MAX_GAMMA), + i=st.integers(0, 1), + inx=st.integers(3 * 10**15, 3 * 10**20), + ) + @settings(max_examples=MAX_EXAMPLES_NOLOSS) + def test_y_noloss(self, A, x, yx, gamma, i, inx): + j = 1 - i + y = x * yx // 10**18 + curve = Curve(A, gamma, 10**18, 2) + curve.x = [x, y] + in_amount = x * inx // 10**18 + try: + out_amount = curve.y(in_amount, i, j) + D1 = curve.D() + except ValueError: + return # Convergence checked separately - we deliberately try unsafe numbers + is_safe = all( + f >= MIN_XD and f <= MAX_XD + for f in [xx * 10**18 // D1 for xx in curve.x] + ) + curve.x[i] = in_amount + curve.x[j] = out_amount + try: + D2 = curve.D() + except ValueError: + return # Convergence checked separately - we deliberately try unsafe numbers + is_safe &= all( + f >= MIN_XD and f <= MAX_XD + for f in [xx * 10**18 // D2 for xx in curve.x] + ) + if is_safe: + assert ( + 2 * (D1 - D2) / (D1 + D2) < MIN_FEE + ) # Only loss is prevented - gain is ok + + @given( + A=st.integers(MIN_A, MAX_A), + D=st.integers(10**18, 10**15 * 10**18), # 1 USD to 1e15 USD + xD=st.integers(MIN_XD, MAX_XD), + yD=st.integers(MIN_XD, MAX_XD), + gamma=st.integers(MIN_GAMMA, MAX_GAMMA), + j=st.integers(0, 1), + ) + @settings(max_examples=MAX_EXAMPLES_YD) + def test_y_from_D(self, A, D, xD, yD, gamma, j): + xp = [D * xD // 10**18, D * yD // 10**18] + y = solve_x(A, gamma, xp, D, j) + xp[j] = y + D2 = solve_D(A, gamma, xp) + assert ( + 2 * (D - D2) / (D2 + D) < MIN_FEE + ) # Only loss is prevented - gain is ok + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unitary/math/test_newton_D.py b/tests/unitary/math/test_newton_D.py index d7f15c5e..0847cc5c 100644 --- a/tests/unitary/math/test_newton_D.py +++ b/tests/unitary/math/test_newton_D.py @@ -4,7 +4,6 @@ from decimal import Decimal import pytest -import yaml from boa.vyper.contract import BoaError from hypothesis import given, settings from hypothesis import strategies as st diff --git a/tests/utils/simulation_int_many.py b/tests/utils/simulation_int_many.py index 57da3bee..179558c4 100644 --- a/tests/utils/simulation_int_many.py +++ b/tests/utils/simulation_int_many.py @@ -2,6 +2,8 @@ # flake8: noqa import json +from tests.unitary.math.misc import get_y_n2_dec + A_MULTIPLIER = 10000 @@ -43,6 +45,8 @@ def newton_D(A, gamma, x, D0): x = sorted(x, reverse=True) N = len(x) + assert N == 2 + for i in range(255): D_prev = D @@ -81,6 +85,8 @@ def newton_D(A, gamma, x, D0): def newton_y(A, gamma, x, D, i): N = len(x) + assert N == 2 + y = D // N K0_i = 10**18 S_i = 0 @@ -128,7 +134,8 @@ def newton_y(A, gamma, x, D, i): def solve_x(A, gamma, x, D, i): - return newton_y(A, gamma, x, D, i) + return get_y_n2_dec(A, gamma, x, D, i) + # return newton_y(A, gamma, x, D, i) def solve_D(A, gamma, x): From 97a644ba2fd0908b56d44ebcd4384d46ea7cdf95 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 1 Nov 2023 12:45:59 +0100 Subject: [PATCH 13/72] begin stateful tests --- contracts/mocks/ERC20Mock.vy | 9 + requirements.txt | 2 +- tests/unitary/pool/stateful/stateful_base.py | 170 ++++++++++++++++++ .../pool/stateful/test_add_liquidity.py | 36 ++++ 4 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 tests/unitary/pool/stateful/stateful_base.py create mode 100644 tests/unitary/pool/stateful/test_add_liquidity.py diff --git a/contracts/mocks/ERC20Mock.vy b/contracts/mocks/ERC20Mock.vy index a7c4b5b1..5f1ace40 100644 --- a/contracts/mocks/ERC20Mock.vy +++ b/contracts/mocks/ERC20Mock.vy @@ -60,3 +60,12 @@ def approve(_spender: address, _value: uint256) -> bool: self.allowances[msg.sender][_spender] = _value log Approval(msg.sender, _spender, _value) return True + + +@external +def _mint_for_testing(_target: address, _value: uint256) -> bool: + self.totalSupply += _value + self.balanceOf[_target] += _value + log Transfer(ZERO_ADDRESS, _target, _value) + + return True \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 919d198b..89b2bded 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,5 @@ pdbpp hypothesis>=6.68.1 # vyper and dev framework: -git+https://github.com/vyperlang/titanoboa@8485787c5196718c679f82e7ac69fef585a4ce1d +git+https://github.com/vyperlang/titanoboa@b5e9fb96d1424ed5cc5a6af03391d885439c83e5 vyper>=0.3.10 diff --git a/tests/unitary/pool/stateful/stateful_base.py b/tests/unitary/pool/stateful/stateful_base.py new file mode 100644 index 00000000..29ccb2fa --- /dev/null +++ b/tests/unitary/pool/stateful/stateful_base.py @@ -0,0 +1,170 @@ +from math import log + +from boa.test import strategy + +MAX_SAMPLES = 20 +MAX_D = 10 ** 12 * 10 ** 18 # $1T is hopefully a reasonable cap for tests +INITIAL_PRICES = [10**18, 1500 * 10**18] # price relative to coin_id = 0 + + +class StatefulBase: + exchange_amount_in = strategy("uint256", max_value=10 ** 9 * 10 ** 18) + exchange_i = strategy("uint8", max_value=1) + sleep_time = strategy("uint256", max_value=86400 * 7) + user = strategy("address") + + def __init__(self, accounts, coins, crypto_swap, token): + self.accounts = accounts + self.swap = crypto_swap + self.coins = coins + self.token = token + + def setup(self, user_id=0): + self.decimals = [int(c.decimals()) for c in self.coins] + self.user_balances = {u: [0] * 2 for u in self.accounts} + self.initial_deposit = [ + 10 ** 4 * 10 ** (18 + d) // p + for p, d in zip([10 ** 18] + INITIAL_PRICES, self.decimals) + ] # $10k * 2 + self.initial_prices = [10 ** 18] + INITIAL_PRICES + user = self.accounts[user_id] + + for coin, q in zip(self.coins, self.initial_deposit): + coin._mint_for_testing(user, q) + coin.approve(self.swap, 2 ** 256 - 1, {"from": user}) + + # Inf approve all, too. Not always that's the best way though + for u in self.accounts: + if u != user: + for coin in self.coins: + coin.approve(self.swap, 2 ** 256 - 1, {"from": u}) + + # Very first deposit + self.swap.add_liquidity(self.initial_deposit, 0, {"from": user}) + + self.balances = self.initial_deposit[:] + self.total_supply = self.token.balanceOf(user) + self.xcp_profit = 10 ** 18 + print(" \n ----------- INIT ----------------- ") + + def convert_amounts(self, amounts): + prices = [10 ** 18] + [self.swap.price_scale()] + return [p * a // 10 ** (36 - d) for p, a, d in zip(prices, amounts, self.decimals)] + + def check_limits(self, amounts, D=True, y=True): + """ + Should be good if within limits, but if outside - can be either + """ + _D = self.swap.D() + prices = [10 ** 18] + [self.swap.price_scale()] + xp_0 = [self.swap.balances(i) for i in range(2)] + xp = xp_0 + xp_0 = [x * p // 10 ** d for x, p, d in zip(xp_0, prices, self.decimals)] + xp = [(x + a) * p // 10 ** d for a, x, p, d in zip(amounts, xp, prices, self.decimals)] + + if D: + for _xp in [xp_0, xp]: + if ( + (min(_xp) * 10 ** 18 // max(_xp) < 10 ** 14) + or (max(_xp) < 10 ** 9 * 10 ** 18) + or (max(_xp) > 10 ** 15 * 10 ** 18) + ): + return False + + if y: + for _xp in [xp_0, xp]: + if ( + (_D < 10 ** 17) + or (_D > 10 ** 15 * 10 ** 18) + or (min(_xp) * 10 ** 18 // _D < 10 ** 16) + or (max(_xp) * 10 ** 18 // _D > 10 ** 20) + ): + return False + + return True + + def rule_exchange(self, exchange_amount_in, exchange_i, user): + return self._rule_exchange(exchange_amount_in, exchange_i, user) + + def _rule_exchange(self, exchange_amount_in, exchange_i, user, check_out_amount=True): + exchange_j = 1 - exchange_i + try: + calc_amount = self.swap.get_dy(exchange_i, exchange_j, exchange_amount_in) + except Exception: + _amounts = [0] * 2 + _amounts[exchange_i] = exchange_amount_in + if self.check_limits(_amounts) and exchange_amount_in > 10000: + raise + return False + self.coins[exchange_i]._mint_for_testing(user, exchange_amount_in) + + d_balance_i = self.coins[exchange_i].balanceOf(user) + d_balance_j = self.coins[exchange_j].balanceOf(user) + try: + self.swap.exchange(exchange_i, exchange_j, exchange_amount_in, 0, {"from": user}) + except Exception: + # Small amounts may fail with rounding errors + if ( + calc_amount > 100 + and exchange_amount_in > 100 + and calc_amount / self.swap.balances(exchange_j) > 1e-13 + and exchange_amount_in / self.swap.balances(exchange_i) > 1e-13 + ): + raise + return False + + # This is to check that we didn't end up in a borked state after + # an exchange succeeded + self.swap.get_dy( + exchange_j, + exchange_i, + 10 ** 16 * 10 ** self.decimals[exchange_j] // ([10 ** 18] + INITIAL_PRICES)[exchange_j], + ) + + d_balance_i -= self.coins[exchange_i].balanceOf(user) + d_balance_j -= self.coins[exchange_j].balanceOf(user) + + assert d_balance_i == exchange_amount_in + if check_out_amount: + if check_out_amount is True: + assert -d_balance_j == calc_amount, f"{-d_balance_j} vs {calc_amount}" + else: + assert abs(d_balance_j + calc_amount) < max( + check_out_amount * calc_amount, 3 + ), f"{-d_balance_j} vs {calc_amount}" + + self.balances[exchange_i] += d_balance_i + self.balances[exchange_j] += d_balance_j + + return True + + def rule_sleep(self, sleep_time): + self.chain.sleep(sleep_time) + + def invariant_balances(self): + balances = [self.swap.balances(i) for i in range(2)] + balances_of = [c.balanceOf(self.swap) for c in self.coins] + for i in range(2): + assert self.balances[i] == balances[i] + assert self.balances[i] == balances_of[i] + + def invariant_total_supply(self): + assert self.total_supply == self.token.totalSupply() + + def invariant_virtual_price(self): + virtual_price = self.swap.virtual_price() + xcp_profit = self.swap.xcp_profit() + get_virtual_price = self.swap.get_virtual_price() + + assert xcp_profit >= 10 ** 18 - 10 + assert virtual_price >= 10 ** 18 - 10 + assert get_virtual_price >= 10 ** 18 - 10 + + assert xcp_profit - self.xcp_profit > -3, f"{xcp_profit} vs {self.xcp_profit}" + assert (virtual_price - 10 ** 18) * 2 - ( + xcp_profit - 10 ** 18 + ) >= -5, f"vprice={virtual_price}, xcp_profit={xcp_profit}" + assert abs(log(virtual_price / get_virtual_price)) < 1e-10 + + self.xcp_profit = xcp_profit + print("INVARIANT UPDATED xcp_profit", self.xcp_profit) diff --git a/tests/unitary/pool/stateful/test_add_liquidity.py b/tests/unitary/pool/stateful/test_add_liquidity.py new file mode 100644 index 00000000..c77ae6e2 --- /dev/null +++ b/tests/unitary/pool/stateful/test_add_liquidity.py @@ -0,0 +1,36 @@ +import math + +import boa +import pytest +from boa.test import strategy +from hypothesis import given, settings + +from tests.fixtures.pool import INITIAL_PRICES +from tests.utils import simulation_int_many as sim +from tests.utils.tokens import mint_for_testing + +SETTINGS = {"max_examples": 100, "deadline": None} + + +@pytest.fixture(scope="module") +def test_deposit(swap, coins, user, fee_receiver): + + quantities = [10**36 // p for p in INITIAL_PRICES] # $3M worth + + for coin, q in zip(coins, quantities): + mint_for_testing(coin, user, q) + with boa.env.prank(user): + coin.approve(swap, 2**256 - 1) + + bal_before = boa.env.get_balance(swap.address) + with boa.env.prank(user): + swap.add_liquidity(quantities, 0) + + # test if eth wasnt deposited: + assert boa.env.get_balance(swap.address) == bal_before + + token_balance = swap.balanceOf(user) + assert ( + token_balance == swap.totalSupply() - swap.balanceOf(fee_receiver) > 0 + ) + assert abs(swap.get_virtual_price() / 1e18 - 1) < 1e-3 From 6ab6f4a9bb3936fa214e37b983fe42b9ccf38ae3 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 1 Nov 2023 12:46:53 +0100 Subject: [PATCH 14/72] fix: rearrange calcs to avoid overflow --- contracts/main/CurveCryptoMathOptimized2.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index dcb98c57..2dd7d79b 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -259,7 +259,7 @@ def get_y( 10**32*3 + 4*gamma*10**14 + gamma2/10**4 - + 4*ANN*gamma2*x_j/D/10000/4/10**4 + + 4*ANN*gamma2/D*x_j/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4 ) From 3c3e29aaaffabc68ac91818c1ae48e7743de33cf Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:07:45 +0100 Subject: [PATCH 15/72] fix: ramp A gamma with old xp[i] value and not new one --- contracts/main/CurveTwocryptoOptimized.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index e62ae376..f6febbff 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -936,7 +936,7 @@ def _exchange( dy: uint256 = 0 y: uint256 = xp[j] - x0: uint256 = xp[i] + x0: uint256 = xp[i] - dx_received # old xp[i] price_scale: uint256 = self.cached_price_scale xp = [ From 4d76e11f55ae7b6f2f6e4755e6efec5c0aa12a8d Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 1 Nov 2023 16:07:40 +0100 Subject: [PATCH 16/72] claim admin virtual balances as well --- contracts/main/CurveTwocryptoOptimized.vy | 12 ++- contracts/mocks/ERC20Mock.vy | 2 +- tests/unitary/pool/stateful/stateful_base.py | 82 ++++++++++++------- .../pool/stateful/test_add_liquidity.py | 2 + 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index f6febbff..a9dbdfff 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -176,6 +176,7 @@ NOISE_FEE: constant(uint256) = 10**5 # <---------------------------- 0.1 BPS. # ----------------------- Admin params --------------------------------------- last_admin_fee_claim_timestamp: uint256 +admin_lp_virtual_balance: uint256 MIN_RAMP_TIME: constant(uint256) = 86400 MIN_ADMIN_FEE_CLAIM_INTERVAL: constant(uint256) = 86400 @@ -698,6 +699,7 @@ def add_liquidity( d_token -= d_token_fee token_supply += d_token self.mint(receiver, d_token) + self.admin_lp_virtual_balance += d_token_fee price_scale = self.tweak_price(A_gamma, xp, D, 0) @@ -1269,13 +1271,16 @@ def _claim_admin_fees(): # ------------------------------ Claim admin fees by minting admin's share # of the pool in LP tokens. - admin_share: uint256 = 0 + + # This is the admin fee tokens claimed in self.add_liquidity. We add it to + # the LP token share that the admin needs to claim: + admin_share: uint256 = self.admin_lp_virtual_balance frac: uint256 = 0 if fee_receiver != empty(address) and fees > 0: # -------------------------------- Calculate admin share to be minted. frac = vprice * 10**18 / (vprice - fees) - 10**18 - admin_share = current_lp_token_supply * frac / 10**18 + admin_share += current_lp_token_supply * frac / 10**18 # ------ Subtract fees from profits that will be used for rebalancing. xcp_profit -= fees * 2 @@ -1295,6 +1300,9 @@ def _claim_admin_fees(): # ---------------------------- Update State ------------------------------ + # Set admin virtual LP balances to zero because we claimed: + self.admin_lp_virtual_balance = 0 + self.xcp_profit = xcp_profit self.last_admin_fee_claim_timestamp = block.timestamp diff --git a/contracts/mocks/ERC20Mock.vy b/contracts/mocks/ERC20Mock.vy index 5f1ace40..c3e50b7a 100644 --- a/contracts/mocks/ERC20Mock.vy +++ b/contracts/mocks/ERC20Mock.vy @@ -68,4 +68,4 @@ def _mint_for_testing(_target: address, _value: uint256) -> bool: self.balanceOf[_target] += _value log Transfer(ZERO_ADDRESS, _target, _value) - return True \ No newline at end of file + return True diff --git a/tests/unitary/pool/stateful/stateful_base.py b/tests/unitary/pool/stateful/stateful_base.py index 29ccb2fa..6cec0a29 100644 --- a/tests/unitary/pool/stateful/stateful_base.py +++ b/tests/unitary/pool/stateful/stateful_base.py @@ -3,12 +3,12 @@ from boa.test import strategy MAX_SAMPLES = 20 -MAX_D = 10 ** 12 * 10 ** 18 # $1T is hopefully a reasonable cap for tests +MAX_D = 10**12 * 10**18 # $1T is hopefully a reasonable cap for tests INITIAL_PRICES = [10**18, 1500 * 10**18] # price relative to coin_id = 0 class StatefulBase: - exchange_amount_in = strategy("uint256", max_value=10 ** 9 * 10 ** 18) + exchange_amount_in = strategy("uint256", max_value=10**9 * 10**18) exchange_i = strategy("uint8", max_value=1) sleep_time = strategy("uint256", max_value=86400 * 7) user = strategy("address") @@ -23,61 +23,69 @@ def setup(self, user_id=0): self.decimals = [int(c.decimals()) for c in self.coins] self.user_balances = {u: [0] * 2 for u in self.accounts} self.initial_deposit = [ - 10 ** 4 * 10 ** (18 + d) // p - for p, d in zip([10 ** 18] + INITIAL_PRICES, self.decimals) + 10**4 * 10 ** (18 + d) // p + for p, d in zip([10**18] + INITIAL_PRICES, self.decimals) ] # $10k * 2 - self.initial_prices = [10 ** 18] + INITIAL_PRICES + self.initial_prices = [10**18] + INITIAL_PRICES user = self.accounts[user_id] for coin, q in zip(self.coins, self.initial_deposit): coin._mint_for_testing(user, q) - coin.approve(self.swap, 2 ** 256 - 1, {"from": user}) + coin.approve(self.swap, 2**256 - 1, {"from": user}) # Inf approve all, too. Not always that's the best way though for u in self.accounts: if u != user: for coin in self.coins: - coin.approve(self.swap, 2 ** 256 - 1, {"from": u}) + coin.approve(self.swap, 2**256 - 1, {"from": u}) # Very first deposit self.swap.add_liquidity(self.initial_deposit, 0, {"from": user}) self.balances = self.initial_deposit[:] self.total_supply = self.token.balanceOf(user) - self.xcp_profit = 10 ** 18 + self.xcp_profit = 10**18 print(" \n ----------- INIT ----------------- ") def convert_amounts(self, amounts): - prices = [10 ** 18] + [self.swap.price_scale()] - return [p * a // 10 ** (36 - d) for p, a, d in zip(prices, amounts, self.decimals)] + prices = [10**18] + [self.swap.price_scale()] + return [ + p * a // 10 ** (36 - d) + for p, a, d in zip(prices, amounts, self.decimals) + ] def check_limits(self, amounts, D=True, y=True): """ Should be good if within limits, but if outside - can be either """ _D = self.swap.D() - prices = [10 ** 18] + [self.swap.price_scale()] + prices = [10**18] + [self.swap.price_scale()] xp_0 = [self.swap.balances(i) for i in range(2)] xp = xp_0 - xp_0 = [x * p // 10 ** d for x, p, d in zip(xp_0, prices, self.decimals)] - xp = [(x + a) * p // 10 ** d for a, x, p, d in zip(amounts, xp, prices, self.decimals)] + xp_0 = [ + x * p // 10**d for x, p, d in zip(xp_0, prices, self.decimals) + ] + xp = [ + (x + a) * p // 10**d + for a, x, p, d in zip(amounts, xp, prices, self.decimals) + ] if D: for _xp in [xp_0, xp]: if ( - (min(_xp) * 10 ** 18 // max(_xp) < 10 ** 14) - or (max(_xp) < 10 ** 9 * 10 ** 18) - or (max(_xp) > 10 ** 15 * 10 ** 18) + (min(_xp) * 10**18 // max(_xp) < 10**14) + or (max(_xp) < 10**9 * 10**18) + or (max(_xp) > 10**15 * 10**18) ): return False if y: for _xp in [xp_0, xp]: if ( - (_D < 10 ** 17) - or (_D > 10 ** 15 * 10 ** 18) - or (min(_xp) * 10 ** 18 // _D < 10 ** 16) - or (max(_xp) * 10 ** 18 // _D > 10 ** 20) + (_D < 10**17) + or (_D > 10**15 * 10**18) + or (min(_xp) * 10**18 // _D < 10**16) + or (max(_xp) * 10**18 // _D > 10**20) ): return False @@ -86,10 +94,14 @@ def check_limits(self, amounts, D=True, y=True): def rule_exchange(self, exchange_amount_in, exchange_i, user): return self._rule_exchange(exchange_amount_in, exchange_i, user) - def _rule_exchange(self, exchange_amount_in, exchange_i, user, check_out_amount=True): + def _rule_exchange( + self, exchange_amount_in, exchange_i, user, check_out_amount=True + ): exchange_j = 1 - exchange_i try: - calc_amount = self.swap.get_dy(exchange_i, exchange_j, exchange_amount_in) + calc_amount = self.swap.get_dy( + exchange_i, exchange_j, exchange_amount_in + ) except Exception: _amounts = [0] * 2 _amounts[exchange_i] = exchange_amount_in @@ -101,7 +113,9 @@ def _rule_exchange(self, exchange_amount_in, exchange_i, user, check_out_amount= d_balance_i = self.coins[exchange_i].balanceOf(user) d_balance_j = self.coins[exchange_j].balanceOf(user) try: - self.swap.exchange(exchange_i, exchange_j, exchange_amount_in, 0, {"from": user}) + self.swap.exchange( + exchange_i, exchange_j, exchange_amount_in, 0, {"from": user} + ) except Exception: # Small amounts may fail with rounding errors if ( @@ -118,7 +132,9 @@ def _rule_exchange(self, exchange_amount_in, exchange_i, user, check_out_amount= self.swap.get_dy( exchange_j, exchange_i, - 10 ** 16 * 10 ** self.decimals[exchange_j] // ([10 ** 18] + INITIAL_PRICES)[exchange_j], + 10**16 + * 10 ** self.decimals[exchange_j] + // ([10**18] + INITIAL_PRICES)[exchange_j], ) d_balance_i -= self.coins[exchange_i].balanceOf(user) @@ -127,7 +143,9 @@ def _rule_exchange(self, exchange_amount_in, exchange_i, user, check_out_amount= assert d_balance_i == exchange_amount_in if check_out_amount: if check_out_amount is True: - assert -d_balance_j == calc_amount, f"{-d_balance_j} vs {calc_amount}" + assert ( + -d_balance_j == calc_amount + ), f"{-d_balance_j} vs {calc_amount}" else: assert abs(d_balance_j + calc_amount) < max( check_out_amount * calc_amount, 3 @@ -156,13 +174,15 @@ def invariant_virtual_price(self): xcp_profit = self.swap.xcp_profit() get_virtual_price = self.swap.get_virtual_price() - assert xcp_profit >= 10 ** 18 - 10 - assert virtual_price >= 10 ** 18 - 10 - assert get_virtual_price >= 10 ** 18 - 10 + assert xcp_profit >= 10**18 - 10 + assert virtual_price >= 10**18 - 10 + assert get_virtual_price >= 10**18 - 10 - assert xcp_profit - self.xcp_profit > -3, f"{xcp_profit} vs {self.xcp_profit}" - assert (virtual_price - 10 ** 18) * 2 - ( - xcp_profit - 10 ** 18 + assert ( + xcp_profit - self.xcp_profit > -3 + ), f"{xcp_profit} vs {self.xcp_profit}" + assert (virtual_price - 10**18) * 2 - ( + xcp_profit - 10**18 ) >= -5, f"vprice={virtual_price}, xcp_profit={xcp_profit}" assert abs(log(virtual_price / get_virtual_price)) < 1e-10 diff --git a/tests/unitary/pool/stateful/test_add_liquidity.py b/tests/unitary/pool/stateful/test_add_liquidity.py index c77ae6e2..85fd488f 100644 --- a/tests/unitary/pool/stateful/test_add_liquidity.py +++ b/tests/unitary/pool/stateful/test_add_liquidity.py @@ -1,3 +1,5 @@ +# flake8: noqa + import math import boa From e601564d41d27617ce95ad42c6021ca752f9fd68 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:58:34 +0100 Subject: [PATCH 17/72] only proportion goes to admin lp virtual balance --- contracts/main/CurveTwocryptoOptimized.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index a9dbdfff..0b8e7b4b 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -699,7 +699,7 @@ def add_liquidity( d_token -= d_token_fee token_supply += d_token self.mint(receiver, d_token) - self.admin_lp_virtual_balance += d_token_fee + self.admin_lp_virtual_balance += unsafe_div(ADMIN_FEE * d_token_fee, 10**10) price_scale = self.tweak_price(A_gamma, xp, D, 0) From 71c58d6fd4b0a56b1f5f7e61a58bb07c98c1810b Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:40:06 +0100 Subject: [PATCH 18/72] introduce spot portfolio and account for portfolio balance in _transfer_in: this prevents hackers from stealing from spot wallets --- contracts/main/CurveCryptoMathOptimized2.vy | 68 +++++++++++++++++++++ contracts/main/CurveTwocryptoOptimized.vy | 23 +++++-- tests/fixtures/accounts.py | 5 ++ tests/unitary/pool/test_spot_wallet.py | 21 +++++++ 4 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 tests/unitary/pool/test_spot_wallet.py diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index 2dd7d79b..bacef933 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -496,3 +496,71 @@ def get_p( _xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[1], _D) * K0, 10**36) ) / _xp[1] * 10**18, denominator ) + + +@external +@pure +def wad_exp(x: int256) -> int256: + """ + @dev Calculates the natural exponential function of a signed integer with + a precision of 1e18. + @notice Note that this function consumes about 810 gas units. The implementation + is inspired by Remco Bloemen's implementation under the MIT license here: + https://xn--2-umb.com/22/exp-ln. + @param x The 32-byte variable. + @return int256 The 32-byte calculation result. + """ + value: int256 = x + + # If the result is `< 0.5`, we return zero. This happens when we have the following: + # "x <= floor(log(0.5e18) * 1e18) ~ -42e18". + if (x <= -42_139_678_854_452_767_551): + return empty(int256) + + # When the result is "> (2 ** 255 - 1) / 1e18" we cannot represent it as a signed integer. + # This happens when "x >= floor(log((2 ** 255 - 1) / 1e18) * 1e18) ~ 135". + assert x < 135_305_999_368_893_231_589, "Math: wad_exp overflow" + + # `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 ** 96" for higher + # intermediate precision and a binary base. This base conversion is a multiplication with + # "1e18 / 2 ** 96 = 5 ** 18 / 2 ** 78". + value = unsafe_div(x << 78, 5 ** 18) + + # Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 ** 96" by factoring out powers of two + # so that "exp(x) = exp(x') * 2 ** k", where `k` is a signer integer. Solving this gives + # "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]". + k: int256 = unsafe_add(unsafe_div(value << 96, 54_916_777_467_707_473_351_141_471_128), 2 ** 95) >> 96 + value = unsafe_sub(value, unsafe_mul(k, 54_916_777_467_707_473_351_141_471_128)) + + # Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic, + # we will multiply by a scaling factor later. + y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1_346_386_616_545_796_478_920_950_773_328), value) >> 96, 57_155_421_227_552_351_082_224_309_758_442) + p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94_201_549_194_550_492_254_356_042_504_812), y) >> 96,\ + 28_719_021_644_029_726_153_956_944_680_412_240), value), 4_385_272_521_454_847_904_659_076_985_693_276 << 96) + + # We leave `p` in the "2 ** 192" base so that we do not have to scale it up + # again for the division. + q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2_855_989_394_907_223_263_936_484_059_900), value) >> 96, 50_020_603_652_535_783_019_961_831_881_945) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 533_845_033_583_426_703_283_633_433_725_380) + q = unsafe_add(unsafe_mul(q, value) >> 96, 3_604_857_256_930_695_427_073_651_918_091_429) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 14_423_608_567_350_463_180_887_372_962_807_573) + q = unsafe_add(unsafe_mul(q, value) >> 96, 26_449_188_498_355_588_339_934_803_723_976_023) + + # The polynomial `q` has no zeros in the range because all its roots are complex. + # No scaling is required, as `p` is already "2 ** 96" too large. Also, + # `r` is in the range "(0.09, 0.25) * 2**96" after the division. + r: int256 = unsafe_div(p, q) + + # To finalise the calculation, we have to multiply `r` by: + # - the scale factor "s = ~6.031367120", + # - the factor "2 ** k" from the range reduction, and + # - the factor "1e18 / 2 ** 96" for the base conversion. + # We do this all at once, with an intermediate result in "2**213" base, + # so that the final right shift always gives a positive value. + + # Note that to circumvent Vyper's safecast feature for the potentially + # negative parameter value `r`, we first convert `r` to `bytes32` and + # subsequently to `uint256`. Remember that the EVM default behaviour is + # to use two's complement representation to handle signed integers. + return convert(unsafe_mul(convert(convert(r, bytes32), uint256), 3_822_833_074_963_236_453_042_738_258_902_158_003_155_416_615_667) >>\ + convert(unsafe_sub(195, k), uint256), int256) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 0b8e7b4b..6dd51ef4 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -154,6 +154,8 @@ balances: public(uint256[N_COINS]) spot_wallet_balances: public(HashMap[address, uint256[N_COINS]]) # <---- Spot # Wallet is a hashmap that stores coin balances for users. This cannot # be commingled with balances. +spot_portfolio: public(uint256[N_COINS]) # <--- A sum of all coin balances in +# all spot wallets. D: public(uint256) xcp_profit: public(uint256) xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees. @@ -296,7 +298,8 @@ def _transfer_in( This is only enabled for exchange_received. @return The amount of tokens received. """ - coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) + # Subtract spot portfolio from coin balances: + coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - self.spot_portfolio[_coin_idx] if expect_optimistic_transfer: # Only enabled in exchange_received: # it expects the caller of exchange_received to have sent tokens to @@ -384,26 +387,30 @@ def deposit_to_spot_wallet(_amounts: uint256[N_COINS], _account: address): # Adjust balances before handling transfers account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] + spot_portfolio: uint256[N_COINS] = self.spot_portfolio # Transfer out of spot wallet coin_balance: uint256 = 0 + received_amount: uint256 = 0 for i in range(N_COINS): if _amounts[i] > 0: coin_balance = ERC20(coins[i]).balanceOf(self) - assert ERC20(coins[i]).transferFrom( _account, self, _amounts[i], default_return_value=True ) + received_amount = ERC20(coins[i]).balanceOf(self) - coin_balance - account_balance[i] += ERC20(coins[i]).balanceOf(self) - coin_balance + account_balance[i] += received_amount + spot_portfolio[i] += received_amount # Update spot wallet account balances self.spot_wallet_balances[_account] = account_balance + self.spot_portfolio = spot_portfolio @external @@ -412,9 +419,15 @@ def withdraw_from_spot_wallet(_amounts: uint256[N_COINS], _account: address): # Adjust balances before handling transfers account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] - account_balance[0] -= _amounts[0] - account_balance[1] -= _amounts[1] + spot_portfolio: uint256[N_COINS] = self.spot_portfolio + + for i in range(N_COINS): + if _amounts[i] > 0: + account_balance[i] -= _amounts[i] + spot_portfolio[i] -= _amounts[i] + self.spot_wallet_balances[_account] = account_balance + self.spot_portfolio = spot_portfolio # Transfer out of spot wallet for i in range(N_COINS): diff --git a/tests/fixtures/accounts.py b/tests/fixtures/accounts.py index f947969c..972ac7b4 100644 --- a/tests/fixtures/accounts.py +++ b/tests/fixtures/accounts.py @@ -15,6 +15,11 @@ def owner(): return boa.env.generate_address() +@pytest.fixture(scope="module") +def hacker(): + return boa.env.generate_address() + + @pytest.fixture(scope="module") def factory_admin(tricrypto_factory): return tricrypto_factory.admin() diff --git a/tests/unitary/pool/test_spot_wallet.py b/tests/unitary/pool/test_spot_wallet.py new file mode 100644 index 00000000..50035ec0 --- /dev/null +++ b/tests/unitary/pool/test_spot_wallet.py @@ -0,0 +1,21 @@ +import boa + +from tests.fixtures.pool import INITIAL_PRICES +from tests.utils.tokens import mint_for_testing + + +def test_no_steal_from_wallet(swap, coins, user, hacker): + quantities = [10**36 // p for p in INITIAL_PRICES] # $3M worth + + for coin, q in zip(coins, quantities): + mint_for_testing(coin, user, q) + with boa.env.prank(user): + coin.approve(swap, 2**256 - 1) + + split_quantities = [quantities[0] // 2, quantities[1] // 2] + with boa.env.prank(user): + swap.add_liquidity(split_quantities, 0) + swap.deposit_to_spot_wallet(split_quantities, user) + + with boa.env.prank(hacker), boa.reverts(dev="user didn't give us coins"): + swap.exchange_received(0, 1, 10**17, 0, hacker) From 05d0b4ffbd62e91c6678018237c80982d60557c9 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:59:26 +0100 Subject: [PATCH 19/72] only msg.sender can deposit or withdraw from spot wallet --- contracts/main/CurveTwocryptoOptimized.vy | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 6dd51ef4..97b677bc 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -383,10 +383,10 @@ def _transfer_from_spot_wallet(_coin_idx: uint256, _amount: uint256, _account: a @external @nonreentrant('lock') -def deposit_to_spot_wallet(_amounts: uint256[N_COINS], _account: address): +def deposit_to_spot_wallet(_amounts: uint256[N_COINS]): # Adjust balances before handling transfers - account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] + account_balance: uint256[N_COINS] = self.spot_wallet_balances[msg.sender] spot_portfolio: uint256[N_COINS] = self.spot_portfolio # Transfer out of spot wallet @@ -398,7 +398,7 @@ def deposit_to_spot_wallet(_amounts: uint256[N_COINS], _account: address): coin_balance = ERC20(coins[i]).balanceOf(self) assert ERC20(coins[i]).transferFrom( - _account, + msg.sender, self, _amounts[i], default_return_value=True @@ -409,16 +409,16 @@ def deposit_to_spot_wallet(_amounts: uint256[N_COINS], _account: address): spot_portfolio[i] += received_amount # Update spot wallet account balances - self.spot_wallet_balances[_account] = account_balance + self.spot_wallet_balances[msg.sender] = account_balance self.spot_portfolio = spot_portfolio @external @nonreentrant('lock') -def withdraw_from_spot_wallet(_amounts: uint256[N_COINS], _account: address): +def withdraw_from_spot_wallet(_amounts: uint256[N_COINS]): # Adjust balances before handling transfers - account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] + account_balance: uint256[N_COINS] = self.spot_wallet_balances[msg.sender] spot_portfolio: uint256[N_COINS] = self.spot_portfolio for i in range(N_COINS): @@ -426,7 +426,7 @@ def withdraw_from_spot_wallet(_amounts: uint256[N_COINS], _account: address): account_balance[i] -= _amounts[i] spot_portfolio[i] -= _amounts[i] - self.spot_wallet_balances[_account] = account_balance + self.spot_wallet_balances[msg.sender] = account_balance self.spot_portfolio = spot_portfolio # Transfer out of spot wallet @@ -435,7 +435,7 @@ def withdraw_from_spot_wallet(_amounts: uint256[N_COINS], _account: address): if _amounts[i] > 0: assert ERC20(coins[i]).transfer( - _account, + msg.sender, _amounts[i], default_return_value=True ) From f901e54d6aea8ffcfecee2662fc80be006fd9f34 Mon Sep 17 00:00:00 2001 From: Filipp <> Date: Sun, 5 Nov 2023 13:45:22 +0300 Subject: [PATCH 20/72] Change range of params for get_y unitary test. --- contracts/experimental/n=2.vy | 8 ++++---- tests/unitary/math/test_get_y.py | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/contracts/experimental/n=2.vy b/contracts/experimental/n=2.vy index e864537c..507f8e25 100644 --- a/contracts/experimental/n=2.vy +++ b/contracts/experimental/n=2.vy @@ -20,9 +20,9 @@ def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: ANN = A * N**N """ # Safety checks - # assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A - # assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma - # assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D + assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A + assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D x_j: uint256 = x[1 - i] y: uint256 = D**2 / (x_j * N_COINS**2) @@ -84,7 +84,7 @@ def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: diff = y_prev - y if diff < max(convergence_limit, y / 10**14): frac: uint256 = y * 10**18 / D - # assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y return y raise "Did not converge" diff --git a/tests/unitary/math/test_get_y.py b/tests/unitary/math/test_get_y.py index 905ab2b0..6f0425b7 100644 --- a/tests/unitary/math/test_get_y.py +++ b/tests/unitary/math/test_get_y.py @@ -11,11 +11,10 @@ A_MUL = 10000 MIN_A = int(N_COINS**N_COINS * A_MUL / 10) -MAX_A = int(N_COINS**N_COINS * A_MUL * 100000) +MAX_A = int(N_COINS**N_COINS * A_MUL * 1000) -# gamma from 1e-8 up to 0.05 MIN_GAMMA = 10**10 -MAX_GAMMA = 5 * 10**16 +MAX_GAMMA = 2 * 10**15 pytest.current_case_id = 0 pytest.negative_sqrt_arg = 0 @@ -53,10 +52,10 @@ def inv_target_decimal_n2(A, gamma, x, D): min_value=10**18, max_value=10**14 * 10**18 ), # 1 USD to 100T USD xD=st.integers( - min_value=int(1.001e16), max_value=int(0.999e20) + min_value=5 * 10**16, max_value=10**19 ), # <- ratio 1e18 * x/D, typically 1e18 * 1 yD=st.integers( - min_value=int(1.001e16), max_value=int(0.999e20) + min_value=5 * 10**16, max_value=10**19 ), # <- ratio 1e18 * y/D, typically 1e18 * 1 gamma=st.integers(min_value=MIN_GAMMA, max_value=MAX_GAMMA), j=st.integers(min_value=0, max_value=1), From 3be3a0f2f63dd90a3bf42a2d1eccec259d641407 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:18:05 +0100 Subject: [PATCH 21/72] fix: error using cached price instead of uncached for price oracle --- contracts/main/CurveTwocryptoOptimized.vy | 63 ++++++++++++++--------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 97b677bc..1298f5af 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -1673,6 +1673,43 @@ def burnFrom(_to: address, _value: uint256) -> bool: # ------------------------- AMM View Functions ------------------------------- +@internal +@view +def internal_price_oracle() -> uint256: + """ + @notice Returns the oracle price of the coin at index `k` w.r.t the coin + at index 0. + @dev The oracle is an exponential moving average, with a periodicity + determined by `self.ma_time`. The aggregated prices are cached state + prices (dy/dx) calculated AFTER the latest trade. + @param k The index of the coin. + @return uint256 Price oracle value of kth coin. + """ + price_oracle: uint256 = self.cached_price_oracle + price_scale: uint256 = self.cached_price_scale + last_prices_timestamp: uint256 = self.last_timestamp[0] + + if last_prices_timestamp < block.timestamp: # <------------ Update moving + # average if needed. + + last_prices: uint256 = self.last_prices + ma_time: uint256 = self._unpack(self.packed_rebalancing_params)[2] + alpha: uint256 = MATH.wad_exp( + -convert( + (block.timestamp - last_prices_timestamp) * 10**18 / ma_time, + int256, + ) + ) + + # ---- We cap state price that goes into the EMA with 2 x price_scale. + return ( + min(last_prices, 2 * price_scale) * (10**18 - alpha) + + price_oracle * alpha + ) / 10**18 + + return price_oracle + + @external @view def fee_receiver() -> address: @@ -1740,7 +1777,7 @@ def lp_price() -> uint256: 0th index @return uint256 LP price. """ - return 2 * self.virtual_price * isqrt(self.cached_price_oracle) / 10**18 + return 2 * self.virtual_price * isqrt(self.internal_price_oracle()) / 10**18 @external @@ -1769,29 +1806,7 @@ def price_oracle() -> uint256: @param k The index of the coin. @return uint256 Price oracle value of kth coin. """ - price_oracle: uint256 = self.cached_price_oracle - price_scale: uint256 = self.cached_price_scale - last_prices_timestamp: uint256 = self.last_timestamp[0] - - if last_prices_timestamp < block.timestamp: # <------------ Update moving - # average if needed. - - last_prices: uint256 = self.last_prices - ma_time: uint256 = self._unpack(self.packed_rebalancing_params)[2] - alpha: uint256 = MATH.wad_exp( - -convert( - (block.timestamp - last_prices_timestamp) * 10**18 / ma_time, - int256, - ) - ) - - # ---- We cap state price that goes into the EMA with 2 x price_scale. - return ( - min(last_prices, 2 * price_scale) * (10**18 - alpha) + - price_oracle * alpha - ) / 10**18 - - return price_oracle + return self.internal_price_oracle() @external From 5eceae47f09bb5ddb1ee71b4ddc821996beb4036 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:22:33 +0100 Subject: [PATCH 22/72] change max limits for gamma and A (found by filipp after rigorous testing --- contracts/main/CurveCryptoMathOptimized2.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index bacef933..fcf3c524 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -17,10 +17,10 @@ N_COINS: constant(uint256) = 2 A_MULTIPLIER: constant(uint256) = 10000 MIN_GAMMA: constant(uint256) = 10**10 -MAX_GAMMA: constant(uint256) = 5 * 10**16 +MAX_GAMMA: constant(uint256) = 2 * 10**16 MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 -MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 100000 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 version: public(constant(String[8])) = "v2.0.0" From 53a8e46ef33d70198592c95fb4a76a4cc7ea9f9f Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:35:19 +0100 Subject: [PATCH 23/72] use pragma instead --- contracts/main/CurveCryptoMathOptimized2.vy | 4 ++-- contracts/main/CurveCryptoViews2Optimized.vy | 2 +- contracts/main/CurveTwocryptoFactory.vy | 3 ++- contracts/main/CurveTwocryptoFactoryHandler.vy | 2 +- contracts/main/CurveTwocryptoOptimized.vy | 4 ++-- contracts/main/LiquidityGauge.vy | 4 ++-- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index fcf3c524..b8fd38fa 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -1,5 +1,5 @@ -# @version 0.3.10 -#pragma optimize gas +# pragma version 0.3.10 +# pragma optimize gas # (c) Curve.Fi, 2020-2023 # AMM Math for 2-coin Curve Cryptoswap Pools # diff --git a/contracts/main/CurveCryptoViews2Optimized.vy b/contracts/main/CurveCryptoViews2Optimized.vy index 87731dd4..145fb1e8 100644 --- a/contracts/main/CurveCryptoViews2Optimized.vy +++ b/contracts/main/CurveCryptoViews2Optimized.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.3.10 """ @title CurveCryptoViews2Optimized @author Curve.Fi diff --git a/contracts/main/CurveTwocryptoFactory.vy b/contracts/main/CurveTwocryptoFactory.vy index d55e16a5..c82baf20 100644 --- a/contracts/main/CurveTwocryptoFactory.vy +++ b/contracts/main/CurveTwocryptoFactory.vy @@ -1,4 +1,5 @@ -# @version 0.3.10 +# pragma version 0.3.10 +# pragma optimize gas """ @title CurveTwocryptoFactory @author Curve.Fi diff --git a/contracts/main/CurveTwocryptoFactoryHandler.vy b/contracts/main/CurveTwocryptoFactoryHandler.vy index f2e0fd05..e949ab50 100644 --- a/contracts/main/CurveTwocryptoFactoryHandler.vy +++ b/contracts/main/CurveTwocryptoFactoryHandler.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.3.10 """ @title CurveTwocryptoFactoryHandler @author Curve.Fi diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 1298f5af..09c17a5a 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -1,5 +1,5 @@ -# @version 0.3.10 -#pragma optimize gas +# pragma version 0.3.10 +# pragma optimize gas """ @title CurveTwocryptoOptimized @author Curve.Fi diff --git a/contracts/main/LiquidityGauge.vy b/contracts/main/LiquidityGauge.vy index 710baa8f..87307c4d 100644 --- a/contracts/main/LiquidityGauge.vy +++ b/contracts/main/LiquidityGauge.vy @@ -1,5 +1,5 @@ -# @version 0.3.10 -#pragma optimize gas +# pragma version 0.3.10 +# pragma optimize gas """ @title LiquidityGaugeV6 @author Curve.Fi From 4352c72041e8f9622bce50458cecb7b823df839e Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:10:19 +0100 Subject: [PATCH 24/72] fix: redesign exchange_received_split to relax out amounts --- contracts/main/CurveTwocryptoOptimized.vy | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 09c17a5a..204c6ea4 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -545,7 +545,7 @@ def exchange_received_split( j: uint256, split_in: uint256[2], # <---- sum of split_in is `dx` (amount_in) min_dy: uint256, - split_out: uint256[2], + amount_to_receiver: uint256, receiver: address, expect_optimistic_transfer: bool ) -> uint256: @@ -572,10 +572,9 @@ def exchange_received_split( expect_optimistic_transfer set to True). Index 1 is what is used from msg.sender's spot balance. @param min_dy Minimum amount of output coin to receive - @param split_out Array of output amounts that are handled by the pool. There are two - elements in the array: index 0 is the amount of coin[j] sent out to - `receiver`. The rest goes into msg.sender's spot wallet balances. - @param receiver Address to send split_out[0] amount of the output coin to + @param amount_to_receiver Amount of coin[j] sent out to `receiver`. The rest goes into + msg.sender's spot wallet balances. + @param receiver Address to send amount_to_receiver amount of the output coin to @param expect_optimistic_transfer If True: user needs to do a transfer into the pool similar to exchange_received, and then call this method. @@ -604,21 +603,24 @@ def exchange_received_split( j, dx_received, min_dy, - ) + ) # <------------------------------------------ min_dy checks occur here. - assert split_out[0] + split_out[1] == out[0] # dev: requested split is greater than calculated dy + # Calculate amount that goes to wallet. amount_to_receiver cannot be + # greater than out[0]: + amount_to_wallet: uint256 = out[0] - amount_to_receiver # Difference between calculated dy and requested out amount is what stays - # in the spot wallet. To make this a fully spot_wallet swap, set split[0] to 0. - if split_out[1] > 0: - self._transfer_to_spot_wallet(j, split_out[1], msg.sender) + # in the spot wallet. To make this a fully spot_wallet swap, set split[0] + # to 0. + if amount_to_receiver > 0: + self._transfer_to_spot_wallet(j, amount_to_receiver, msg.sender) # _transfer_out updates self.balances here. Update to state occurs before # external calls: - if split_out[0] > 0: - self._transfer_out(j, split_out[0], receiver) - # log: + if amount_to_wallet > 0: + self._transfer_out(j, amount_to_wallet, receiver) + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) return out[0] From 026afab1e194bc72fc7257135a21cba62f10be06 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:13:40 +0100 Subject: [PATCH 25/72] fix: use correct amounts --- contracts/main/CurveTwocryptoOptimized.vy | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 204c6ea4..262abf47 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -608,18 +608,13 @@ def exchange_received_split( # Calculate amount that goes to wallet. amount_to_receiver cannot be # greater than out[0]: amount_to_wallet: uint256 = out[0] - amount_to_receiver - - # Difference between calculated dy and requested out amount is what stays - # in the spot wallet. To make this a fully spot_wallet swap, set split[0] - # to 0. - if amount_to_receiver > 0: - self._transfer_to_spot_wallet(j, amount_to_receiver, msg.sender) + if amount_to_wallet > 0: + self._transfer_to_spot_wallet(j, amount_to_wallet, msg.sender) # _transfer_out updates self.balances here. Update to state occurs before # external calls: - - if amount_to_wallet > 0: - self._transfer_out(j, amount_to_wallet, receiver) + if amount_to_receiver > 0: + self._transfer_out(j, amount_to_receiver, receiver) log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) From 5500457059b430764cf9326a7fc49f33d25ce124 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:05:06 +0100 Subject: [PATCH 26/72] goodbye spot wallet idea. you were good but not good enough --- contracts/main/CurveTwocryptoOptimized.vy | 180 +--------------------- 1 file changed, 2 insertions(+), 178 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 262abf47..e7905af7 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -151,11 +151,6 @@ future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. # and not set to 0. balances: public(uint256[N_COINS]) -spot_wallet_balances: public(HashMap[address, uint256[N_COINS]]) # <---- Spot -# Wallet is a hashmap that stores coin balances for users. This cannot -# be commingled with balances. -spot_portfolio: public(uint256[N_COINS]) # <--- A sum of all coin balances in -# all spot wallets. D: public(uint256) xcp_profit: public(uint256) xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees. @@ -298,8 +293,7 @@ def _transfer_in( This is only enabled for exchange_received. @return The amount of tokens received. """ - # Subtract spot portfolio from coin balances: - coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - self.spot_portfolio[_coin_idx] + coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) if expect_optimistic_transfer: # Only enabled in exchange_received: # it expects the caller of exchange_received to have sent tokens to @@ -356,91 +350,6 @@ def _transfer_out(_coin_idx: uint256, _amount: uint256, receiver: address): ) -# ------------- Token transfers in and out involving Spot Wallets ------------ -# NOTE: EXPERIMENTAL - -@internal -def _transfer_to_spot_wallet(_coin_idx: uint256, _amount: uint256, _account: address): - - account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] - - # Adjust balances before handling transfers: - self.balances[_coin_idx] -= _amount - account_balance[_coin_idx] += _amount - self.spot_wallet_balances[_account] = account_balance - - -@internal -def _transfer_from_spot_wallet(_coin_idx: uint256, _amount: uint256, _account: address): - - account_balance: uint256[N_COINS] = self.spot_wallet_balances[_account] - - # Adjust balances before handling transfers: - self.balances[_coin_idx] += _amount - account_balance[_coin_idx] -= _amount - self.spot_wallet_balances[_account] = account_balance - - -@external -@nonreentrant('lock') -def deposit_to_spot_wallet(_amounts: uint256[N_COINS]): - - # Adjust balances before handling transfers - account_balance: uint256[N_COINS] = self.spot_wallet_balances[msg.sender] - spot_portfolio: uint256[N_COINS] = self.spot_portfolio - - # Transfer out of spot wallet - coin_balance: uint256 = 0 - received_amount: uint256 = 0 - for i in range(N_COINS): - - if _amounts[i] > 0: - - coin_balance = ERC20(coins[i]).balanceOf(self) - assert ERC20(coins[i]).transferFrom( - msg.sender, - self, - _amounts[i], - default_return_value=True - ) - received_amount = ERC20(coins[i]).balanceOf(self) - coin_balance - - account_balance[i] += received_amount - spot_portfolio[i] += received_amount - - # Update spot wallet account balances - self.spot_wallet_balances[msg.sender] = account_balance - self.spot_portfolio = spot_portfolio - - -@external -@nonreentrant('lock') -def withdraw_from_spot_wallet(_amounts: uint256[N_COINS]): - - # Adjust balances before handling transfers - account_balance: uint256[N_COINS] = self.spot_wallet_balances[msg.sender] - spot_portfolio: uint256[N_COINS] = self.spot_portfolio - - for i in range(N_COINS): - if _amounts[i] > 0: - account_balance[i] -= _amounts[i] - spot_portfolio[i] -= _amounts[i] - - self.spot_wallet_balances[msg.sender] = account_balance - self.spot_portfolio = spot_portfolio - - # Transfer out of spot wallet - for i in range(N_COINS): - - if _amounts[i] > 0: - - assert ERC20(coins[i]).transfer( - msg.sender, - _amounts[i], - default_return_value=True - ) - - # -------------------------- AMM Main Functions ------------------------------ @@ -537,90 +446,6 @@ def exchange_received( return out[0] -# NOTE: EXPERIMENTAL -@external -@nonreentrant('lock') -def exchange_received_split( - i: uint256, - j: uint256, - split_in: uint256[2], # <---- sum of split_in is `dx` (amount_in) - min_dy: uint256, - amount_to_receiver: uint256, - receiver: address, - expect_optimistic_transfer: bool -) -> uint256: - """ - @notice Exchange: but user must transfer dx amount of coin[i] tokens to pool first. - Pool will not call transferFrom and will only check if a surplus of - coins[i] is greater than or equal to `dx`. - User also needs to specify a `split` to decide what amount of dy_out goes - to receiver address and what amount stays in the sender's spot wallet. - @dev Use-case is to reduce the number of redundant ERC20 token - transfers in zaps. Primarily for dex-aggregators/arbitrageurs/searchers, - and helps reduce the total number of ERC20 tokens per arb transaction to - 2: arbitrageur can withdraw profits at a later time and do not need to hedge - atomically (which is expensive). - Note for users: please transfer + exchange_received_split in 1 tx if - expect_optimistic_transfer is set to True. - @param i Index value for the input coin - @param j Index value for the output coin - @param split_in Amount of input coin being swapped in. - Index 0 is what is transferred in from outside the pool contract. - This can occur using either transferFrom if expect_optimistic_transfer - is set to False, else the pool contract expects user to transfer into - the pool (in the same tx) and then call exchange_received_split (with - expect_optimistic_transfer set to True). - Index 1 is what is used from msg.sender's spot balance. - @param min_dy Minimum amount of output coin to receive - @param amount_to_receiver Amount of coin[j] sent out to `receiver`. The rest goes into - msg.sender's spot wallet balances. - @param receiver Address to send amount_to_receiver amount of the output coin to - @param expect_optimistic_transfer If True: user needs to do a transfer into the pool - similar to exchange_received, and then call this - method. - @return uint256 Amount of tokens at index j received by the `receiver` - """ - dx_received: uint256 = 0 - if split_in[0] > 0: - # Two cases: - # 1. expect_optimistic_transfer is set to True. Use case: flashswap from - # another AMM, set this twocrypto-ng pool as receiver. - # 2. pool calls ERC20(coins[i]).transferFrom(msg.sender, self, dx) - dx_received = self._transfer_in( - i, - split_in[0], - msg.sender, - expect_optimistic_transfer - ) - - if split_in[1] > 0: - self._transfer_from_spot_wallet(i, split_in[1], msg.sender) - dx_received += split_in[1] - - # No ERC20 token transfers occur here: - out: uint256[3] = self._exchange( - i, - j, - dx_received, - min_dy, - ) # <------------------------------------------ min_dy checks occur here. - - # Calculate amount that goes to wallet. amount_to_receiver cannot be - # greater than out[0]: - amount_to_wallet: uint256 = out[0] - amount_to_receiver - if amount_to_wallet > 0: - self._transfer_to_spot_wallet(j, amount_to_wallet, msg.sender) - - # _transfer_out updates self.balances here. Update to state occurs before - # external calls: - if amount_to_receiver > 0: - self._transfer_out(j, amount_to_receiver, receiver) - - log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) - - return out[0] - - @external @nonreentrant("lock") def add_liquidity( @@ -1263,8 +1088,7 @@ def _claim_admin_fees(): vprice: uint256 = self.virtual_price price_scale: uint256 = self.cached_price_scale fee_receiver: address = factory.fee_receiver() - balances: uint256[N_COINS] = self.balances # <- since there's no gulping, - # admin cannot commingle user spot balance with pool balances. + balances: uint256[N_COINS] = self.balances # Admin fees are calculated as follows. # 1. Calculate accrued profit since last claim. `xcp_profit` From 0e33774bd6c10f021452c08c100b817b4e905c7f Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:24:12 +0100 Subject: [PATCH 27/72] fix limits and remove spot wallet tests --- contracts/main/CurveCryptoMathOptimized2.vy | 2 +- tests/unitary/math/fuzz_multicoin_curve.py | 4 ++-- tests/unitary/pool/test_exchange_received.py | 2 +- tests/unitary/pool/test_spot_wallet.py | 21 -------------------- 4 files changed, 4 insertions(+), 25 deletions(-) delete mode 100644 tests/unitary/pool/test_spot_wallet.py diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index b8fd38fa..e3eacd5b 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -17,7 +17,7 @@ N_COINS: constant(uint256) = 2 A_MULTIPLIER: constant(uint256) = 10000 MIN_GAMMA: constant(uint256) = 10**10 -MAX_GAMMA: constant(uint256) = 2 * 10**16 +MAX_GAMMA: constant(uint256) = 2 * 10**15 MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 diff --git a/tests/unitary/math/fuzz_multicoin_curve.py b/tests/unitary/math/fuzz_multicoin_curve.py index fe75c118..4575cb07 100644 --- a/tests/unitary/math/fuzz_multicoin_curve.py +++ b/tests/unitary/math/fuzz_multicoin_curve.py @@ -27,11 +27,11 @@ N_COINS = 2 A_MUL = 10000 MIN_A = int(N_COINS**N_COINS * A_MUL / 10) -MAX_A = int(N_COINS**N_COINS * A_MUL * 100000) +MAX_A = int(N_COINS**N_COINS * A_MUL * 1000) # gamma from 1e-8 up to 0.05 MIN_GAMMA = 10**10 -MAX_GAMMA = 2 * 10**16 +MAX_GAMMA = 2 * 10**15 # Test with 2 coins diff --git a/tests/unitary/pool/test_exchange_received.py b/tests/unitary/pool/test_exchange_received.py index ab54e852..de2ab754 100644 --- a/tests/unitary/pool/test_exchange_received.py +++ b/tests/unitary/pool/test_exchange_received.py @@ -19,7 +19,7 @@ j=strategy("uint", min_value=0, max_value=1), ) @settings(**SETTINGS) -def test_exchange_split( +def test_exchange_received( swap_with_deposit, views_contract, coins, diff --git a/tests/unitary/pool/test_spot_wallet.py b/tests/unitary/pool/test_spot_wallet.py deleted file mode 100644 index 50035ec0..00000000 --- a/tests/unitary/pool/test_spot_wallet.py +++ /dev/null @@ -1,21 +0,0 @@ -import boa - -from tests.fixtures.pool import INITIAL_PRICES -from tests.utils.tokens import mint_for_testing - - -def test_no_steal_from_wallet(swap, coins, user, hacker): - quantities = [10**36 // p for p in INITIAL_PRICES] # $3M worth - - for coin, q in zip(coins, quantities): - mint_for_testing(coin, user, q) - with boa.env.prank(user): - coin.approve(swap, 2**256 - 1) - - split_quantities = [quantities[0] // 2, quantities[1] // 2] - with boa.env.prank(user): - swap.add_liquidity(split_quantities, 0) - swap.deposit_to_spot_wallet(split_quantities, user) - - with boa.env.prank(hacker), boa.reverts(dev="user didn't give us coins"): - swap.exchange_received(0, 1, 10**17, 0, hacker) From d102eaf800c5a4ae531af20320900799e5f0a059 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:24:35 +0100 Subject: [PATCH 28/72] remove ape --- ape-config.yaml | 59 -------------------------------------------- hardhat.config.js | 16 ------------ requirements_ape.txt | 20 --------------- 3 files changed, 95 deletions(-) delete mode 100644 ape-config.yaml delete mode 100644 hardhat.config.js delete mode 100644 requirements_ape.txt diff --git a/ape-config.yaml b/ape-config.yaml deleted file mode 100644 index 33b4e7ae..00000000 --- a/ape-config.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: curve-tricrypto-ng -contracts_folder: contracts/main/ - -plugins: - - name: vyper - - name: alchemy - - name: hardhat - - name: ledger - - name: etherscan - - name: arbitrum - - name: optimism - - name: polygon - -default_ecosystem: ethereum - -# vyper: -# evm_version: paris # enable for non PUSH0 evm networks - -hardhat: - port: auto - fork: - ethereum: - mainnet: - upstream_provider: alchemy - sepolia: - upstream_provider: alchemy - arbitrum: - mainnet: - upstream_provider: geth - -ethereum: - default_network: mainnet-fork - mainnet_fork: - transaction_acceptance_timeout: 99999999 - default_provider: hardhat - mainnet: - transaction_acceptance_timeout: 99999999 - sepolia: - transaction_acceptance_timeout: 99999999 - -arbitrum: - default_network: mainnet-fork - mainnet_fork: - transaction_acceptance_timeout: 99999999 - default_provider: hardhat - mainnet: - transaction_acceptance_timeout: 99999999 - -geth: - ethereum: - mainnet: - uri: http://localhost:9090 - arbitrum: - mainnet: - uri: https://arb-mainnet.g.alchemy.com/v2/{some_key} - -test: - mnemonic: test test test test test test test test test test test junk - number_of_accounts: 5 diff --git a/hardhat.config.js b/hardhat.config.js deleted file mode 100644 index d2d23fda..00000000 --- a/hardhat.config.js +++ /dev/null @@ -1,16 +0,0 @@ - -// See https://hardhat.org/config/ for config options. -module.exports = { - networks: { - hardhat: { - hardfork: "shanghai", - // Base fee of 0 allows use of 0 gas price when testing - initialBaseFeePerGas: 0, - accounts: { - mnemonic: "test test test test test test test test test test test junk", - path: "m/44'/60'/0'", - count: 5 - } - }, - }, -}; diff --git a/requirements_ape.txt b/requirements_ape.txt deleted file mode 100644 index 90a7bd0b..00000000 --- a/requirements_ape.txt +++ /dev/null @@ -1,20 +0,0 @@ -# testing: -ipython -pytest -pdbpp - -# vyper and dev framework: -git+https://github.com/vyperlang/titanoboa@c1d741c26b34798ec1620859c7f3d8f42416e4be -vyper>=0.3.9 -eth-ape==0.6.19 -ape-etherscan==0.6.10 -ape-hardhat==0.6.12 -ape-alchemy==0.6.4 -ape-vyper==0.6.10 -python-dotenv==1.0.0 - -# prices api: -pycoingecko - -# linting: -pre-commit From 6a22fe49beab999358bce9aa78969f7168508d42 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:26:48 +0100 Subject: [PATCH 29/72] remove mentions of tricrypto --- contracts/main/CurveCryptoMathOptimized2.vy | 2 +- tests/fixtures/accounts.py | 4 ++-- tests/fixtures/factory.py | 2 +- tests/fixtures/pool.py | 16 ++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index e3eacd5b..0e4f6b1e 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -7,7 +7,7 @@ # Swiss Stake GmbH are allowed to call this contract. """ -@title CurveTricryptoMathOptimized +@title CurveTwocryptoMathOptimized @author Curve.Fi @license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved @notice Curve AMM Math for 2 unpegged assets (e.g. ETH <> USD). diff --git a/tests/fixtures/accounts.py b/tests/fixtures/accounts.py index 972ac7b4..278e8ca1 100644 --- a/tests/fixtures/accounts.py +++ b/tests/fixtures/accounts.py @@ -21,8 +21,8 @@ def hacker(): @pytest.fixture(scope="module") -def factory_admin(tricrypto_factory): - return tricrypto_factory.admin() +def factory_admin(twocrypto_factory): + return twocrypto_factory.admin() @pytest.fixture(scope="module") diff --git a/tests/fixtures/factory.py b/tests/fixtures/factory.py index 338f542e..cf3a8986 100644 --- a/tests/fixtures/factory.py +++ b/tests/fixtures/factory.py @@ -37,7 +37,7 @@ def views_contract(deployer): @pytest.fixture(scope="module") -def tricrypto_factory( +def twocrypto_factory( deployer, fee_receiver, owner, diff --git a/tests/fixtures/pool.py b/tests/fixtures/pool.py index b77d7541..26f7acb5 100644 --- a/tests/fixtures/pool.py +++ b/tests/fixtures/pool.py @@ -20,7 +20,7 @@ def _get_deposit_amounts(amount_per_token_usd, initial_prices, coins): def _crypto_swap_with_deposit( coins, user, - tricrypto_swap, + twocrypto_swap, initial_prices, dollar_amt_each_coin=int(1.5 * 10**6), ): @@ -38,13 +38,13 @@ def _crypto_swap_with_deposit( # approve crypto_swap to trade coin for user: with boa.env.prank(user): - coin.approve(tricrypto_swap, 2**256 - 1) + coin.approve(twocrypto_swap, 2**256 - 1) # Very first deposit with boa.env.prank(user): - tricrypto_swap.add_liquidity(quantities, 0) + twocrypto_swap.add_liquidity(quantities, 0) - return tricrypto_swap + return twocrypto_swap @pytest.fixture(scope="module") @@ -66,7 +66,7 @@ def params(): @pytest.fixture(scope="module") def swap( - tricrypto_factory, + twocrypto_factory, amm_interface, coins, params, @@ -74,7 +74,7 @@ def swap( ): with boa.env.prank(deployer): - swap = tricrypto_factory.deploy_pool( + swap = twocrypto_factory.deploy_pool( "Curve.fi USD<>WETH", "USD<>WETH", [coin.address for coin in coins], @@ -95,7 +95,7 @@ def swap( @pytest.fixture(scope="module") def swap_multiprecision( - tricrypto_factory, + twocrypto_factory, amm_interface, stgusdc, deployer, @@ -116,7 +116,7 @@ def swap_multiprecision( } with boa.env.prank(deployer): - swap = tricrypto_factory.deploy_pool( + swap = twocrypto_factory.deploy_pool( "Curve.fi STG/USDC", "STGUSDC", [coin.address for coin in stgusdc], From 70c89a4479ef2275e5c2454486640eba7f6843a7 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:28:43 +0100 Subject: [PATCH 30/72] remove k reference --- contracts/main/CurveTwocryptoOptimized.vy | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index e7905af7..91ab1fa6 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -1624,7 +1624,6 @@ def price_oracle() -> uint256: @dev The oracle is an exponential moving average, with a periodicity determined by `self.ma_time`. The aggregated prices are cached state prices (dy/dx) calculated AFTER the latest trade. - @param k The index of the coin. @return uint256 Price oracle value of kth coin. """ return self.internal_price_oracle() @@ -1671,7 +1670,6 @@ def price_scale() -> uint256: at index 0. @dev Price scale determines the price band around which liquidity is concentrated. - @param k The index of the coin. @return uint256 Price scale of coin. """ return self.cached_price_scale From 4cabeebb5cd98a14e66e141a800986bd5a263606 Mon Sep 17 00:00:00 2001 From: Filipp <> Date: Sun, 12 Nov 2023 12:55:52 +0300 Subject: [PATCH 31/72] Fix get_y operation order for precision and overflow prevention. --- contracts/main/CurveCryptoMathOptimized2.vy | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index 0e4f6b1e..82a9a5fd 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -208,9 +208,9 @@ def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256: # Safety checks - assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A - assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma - assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D + # assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A + # assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + # assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D y: uint256 = self._newton_y(ANN, gamma, x, D, i) frac: uint256 = y * 10**18 / D @@ -230,9 +230,9 @@ def get_y( ) -> uint256[2]: # Safety checks - assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A - assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma - assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D + # assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A + # assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + # assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D ANN: int256 = convert(_ANN, int256) gamma: int256 = convert(_gamma, int256) @@ -259,7 +259,7 @@ def get_y( 10**32*3 + 4*gamma*10**14 + gamma2/10**4 - + 4*ANN*gamma2/D*x_j/10000/4/10**4 + + 4*ANN*gamma2/10000/4/10**4*x_j/D - 4*ANN*gamma2/10000/4/10**4 ) @@ -343,7 +343,7 @@ def get_y( C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18) # root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here. - root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_div(unsafe_mul(10**18, b)*delta0, C1))/unsafe_mul(3, a) + root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a) # return [ # convert(D**2/x_j*root/4/10**18, uint256), # <--- y @@ -354,6 +354,7 @@ def get_y( frac: uint256 = y_out[0] * 10**18 / _D assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y + # return [a, b, c, d, delta0, delta1, C1, b_cbrt, second_cbrt, D**2/x_j*root/4/10**18] return y_out From b0079e37633ce0e9be91daeea7dbc3bce61af92f Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:42:14 +0100 Subject: [PATCH 32/72] add test for fixed precision issues --- tests/unitary/math/test_get_y.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unitary/math/test_get_y.py b/tests/unitary/math/test_get_y.py index 6f0425b7..cefbab13 100644 --- a/tests/unitary/math/test_get_y.py +++ b/tests/unitary/math/test_get_y.py @@ -99,3 +99,17 @@ def calculate_F_by_y0(y0): ) or abs(calculate_F_by_y0(result_get_y)) <= abs( calculate_F_by_y0(result_original) ) + + +def test_get_y_revert(math_contract): + a = 1723894848 + gamma = 24009999997600 + x = [ + 112497148627520223862735198942112, + 112327102289152450435452075003508 + ] + D = 224824250915890636214130540882688 + i = 0 + + y_out = math_contract.newton_y(a, gamma, x, D, i) + y_out = math_contract.get_y(a, gamma, x, D, i) From 2f3f93159ad09e37b8fd6f06d5b215cf0f62bbcb Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:28:10 +0100 Subject: [PATCH 33/72] pack last timestamp into one uint256 and add tests --- contracts/main/CurveTwocryptoFactory.vy | 8 +-- contracts/main/CurveTwocryptoOptimized.vy | 62 ++++++++++++++--------- tests/unitary/math/test_get_y.py | 11 ++-- tests/unitary/math/test_packing.py | 29 +++++++++++ 4 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 tests/unitary/math/test_packing.py diff --git a/contracts/main/CurveTwocryptoFactory.vy b/contracts/main/CurveTwocryptoFactory.vy index c82baf20..7a012b9a 100644 --- a/contracts/main/CurveTwocryptoFactory.vy +++ b/contracts/main/CurveTwocryptoFactory.vy @@ -112,7 +112,7 @@ def __init__(_fee_receiver: address, _admin: address): @internal @view -def _pack(x: uint256[3]) -> uint256: +def _pack_3(x: uint256[3]) -> uint256: """ @notice Packs 3 integers with values <= 10**18 into a uint256 @param x The uint256[3] to pack @@ -181,15 +181,15 @@ def deploy_pool( d: uint256 = ERC20(_coins[i]).decimals() assert d < 19, "Max 18 decimals for coins" decimals[i] = d - precisions[i] = 10** (18 - d) + precisions[i] = 10 ** (18 - d) # pack fees - packed_fee_params: uint256 = self._pack( + packed_fee_params: uint256 = self._pack_3( [mid_fee, out_fee, fee_gamma] ) # pack liquidity rebalancing params - packed_rebalancing_params: uint256 = self._pack( + packed_rebalancing_params: uint256 = self._pack_3( [allowed_extra_profit, adjustment_step, ma_exp_time] ) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 91ab1fa6..5a7ce96e 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -135,7 +135,7 @@ cached_price_oracle: uint256 # <------- Price target given by moving average. cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. last_prices: public(uint256) -last_timestamp: public(uint256[2]) # idx 0 is for prices, idx 1 is for xcp. +last_timestamp: public(uint256) # idx 0 is for prices, idx 1 is for xcp. last_xcp: public(uint256) xcp_ma_time: public(uint256) @@ -247,7 +247,7 @@ def __init__( self.cached_price_scale = initial_price self.cached_price_oracle = initial_price self.last_prices = initial_price - self.last_timestamp = [block.timestamp, block.timestamp] + self.last_timestamp = self._pack_2(block.timestamp, block.timestamp) self.xcp_profit_a = 10**18 self.xcp_ma_time = 62324 # <--------- 12 hours default on contract start. @@ -638,7 +638,7 @@ def remove_liquidity( xp: uint256[N_COINS] = self.xp(self.balances, self.cached_price_scale) self.last_xcp = isqrt(xp[0] * xp[1]) - last_timestamp: uint256[2] = self.last_timestamp + last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) if last_timestamp[1] < block.timestamp: cached_xcp_oracle: uint256 = self.cached_xcp_oracle @@ -659,7 +659,7 @@ def remove_liquidity( last_timestamp[1] = block.timestamp # Pack and store timestamps: - self.last_timestamp = last_timestamp + self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) return withdraw_amounts @@ -730,7 +730,7 @@ def remove_liquidity_one_coin( @internal @pure -def _pack(x: uint256[3]) -> uint256: +def _pack_3(x: uint256[3]) -> uint256: """ @notice Packs 3 integers with values <= 10**18 into a uint256 @param x The uint256[3] to pack @@ -741,7 +741,7 @@ def _pack(x: uint256[3]) -> uint256: @internal @pure -def _unpack(_packed: uint256) -> uint256[3]: +def _unpack_3(_packed: uint256) -> uint256[3]: """ @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) @param val The uint256 to unpack @@ -754,6 +754,20 @@ def _unpack(_packed: uint256) -> uint256[3]: ] +@pure +@internal +def _pack_2(p1: uint256, p2: uint256) -> uint256: + assert p1 < 2**128 + assert p2 < 2**128 + return p1 | (p2 << 128) + + +@pure +@internal +def _unpack_2(packed: uint256) -> uint256[2]: + return [packed & (2**128 - 1), packed >> 128] + + # ---------------------- AMM Internal Functions ------------------------------- @@ -849,7 +863,7 @@ def tweak_price( price_oracle: uint256 = self.cached_price_oracle last_prices: uint256 = self.last_prices price_scale: uint256 = self.cached_price_scale - rebalancing_params: uint256[3] = self._unpack(self.packed_rebalancing_params) + rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params) # Contains: allowed_extra_profit, adjustment_step, ma_time. -----^ total_supply: uint256 = self.totalSupply @@ -858,7 +872,7 @@ def tweak_price( # ----------------------- Update Oracles if needed ----------------------- - last_timestamp: uint256[2] = self.last_timestamp + last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) alpha: uint256 = 0 if last_timestamp[0] < block.timestamp: # 0th index is for price_oracle. @@ -914,7 +928,7 @@ def tweak_price( # Pack and store timestamps: last_timestamp[1] = block.timestamp - self.last_timestamp = last_timestamp + self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) # `price_oracle` is used further on to calculate its vector distance from # price_scale. This distance is used to calculate the amount of adjustment @@ -1211,7 +1225,7 @@ def _A_gamma() -> uint256[2]: @view def _fee(xp: uint256[N_COINS]) -> uint256: - fee_params: uint256[3] = self._unpack(self.packed_fee_params) + fee_params: uint256[3] = self._unpack_3(self.packed_fee_params) f: uint256 = xp[0] + xp[1] f = fee_params[2] * 10**18 / ( fee_params[2] + 10**18 - @@ -1308,7 +1322,7 @@ def _calc_withdraw_one_coin( xp_imprecise: uint256[N_COINS] = xp xp_correction: uint256 = xp[i] * N_COINS * token_amount / token_supply - fee: uint256 = self._unpack(self.packed_fee_params)[1] # <- self.out_fee. + fee: uint256 = self._unpack_3(self.packed_fee_params)[1] # <- self.out_fee. if xp_correction < xp_imprecise[i]: xp_imprecise[i] -= xp_correction @@ -1508,13 +1522,13 @@ def internal_price_oracle() -> uint256: """ price_oracle: uint256 = self.cached_price_oracle price_scale: uint256 = self.cached_price_scale - last_prices_timestamp: uint256 = self.last_timestamp[0] + last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[0] if last_prices_timestamp < block.timestamp: # <------------ Update moving # average if needed. last_prices: uint256 = self.last_prices - ma_time: uint256 = self._unpack(self.packed_rebalancing_params)[2] + ma_time: uint256 = self._unpack_3(self.packed_rebalancing_params)[2] alpha: uint256 = MATH.wad_exp( -convert( (block.timestamp - last_prices_timestamp) * 10**18 / ma_time, @@ -1644,7 +1658,7 @@ def xcp_oracle() -> uint256: @return uint256 Oracle value of xcp. """ - last_prices_timestamp: uint256 = self.last_timestamp[1] + last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[1] cached_xcp_oracle: uint256 = self.cached_xcp_oracle if last_prices_timestamp < block.timestamp: @@ -1747,7 +1761,7 @@ def mid_fee() -> uint256: @notice Returns the current mid fee @return uint256 mid_fee value. """ - return self._unpack(self.packed_fee_params)[0] + return self._unpack_3(self.packed_fee_params)[0] @view @@ -1757,7 +1771,7 @@ def out_fee() -> uint256: @notice Returns the current out fee @return uint256 out_fee value. """ - return self._unpack(self.packed_fee_params)[1] + return self._unpack_3(self.packed_fee_params)[1] @view @@ -1767,7 +1781,7 @@ def fee_gamma() -> uint256: @notice Returns the current fee gamma @return uint256 fee_gamma value. """ - return self._unpack(self.packed_fee_params)[2] + return self._unpack_3(self.packed_fee_params)[2] @view @@ -1777,7 +1791,7 @@ def allowed_extra_profit() -> uint256: @notice Returns the current allowed extra profit @return uint256 allowed_extra_profit value. """ - return self._unpack(self.packed_rebalancing_params)[0] + return self._unpack_3(self.packed_rebalancing_params)[0] @view @@ -1787,7 +1801,7 @@ def adjustment_step() -> uint256: @notice Returns the current adjustment step @return uint256 adjustment_step value. """ - return self._unpack(self.packed_rebalancing_params)[1] + return self._unpack_3(self.packed_rebalancing_params)[1] @view @@ -1799,7 +1813,7 @@ def ma_time() -> uint256: One can expect off-by-one errors here. @return uint256 ma_time value. """ - return self._unpack(self.packed_rebalancing_params)[2] * 694 / 1000 + return self._unpack_3(self.packed_rebalancing_params)[2] * 694 / 1000 @view @@ -1937,7 +1951,7 @@ def apply_new_parameters( new_out_fee: uint256 = _new_out_fee new_fee_gamma: uint256 = _new_fee_gamma - current_fee_params: uint256[3] = self._unpack(self.packed_fee_params) + current_fee_params: uint256[3] = self._unpack_3(self.packed_fee_params) if new_out_fee < MAX_FEE + 1: assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range @@ -1953,7 +1967,7 @@ def apply_new_parameters( else: new_fee_gamma = current_fee_params[2] - self.packed_fee_params = self._pack([new_mid_fee, new_out_fee, new_fee_gamma]) + self.packed_fee_params = self._pack_3([new_mid_fee, new_out_fee, new_fee_gamma]) # ----------------- Set liquidity rebalancing parameters ----------------- @@ -1961,7 +1975,7 @@ def apply_new_parameters( new_adjustment_step: uint256 = _new_adjustment_step new_ma_time: uint256 = _new_ma_time - current_rebalancing_params: uint256[3] = self._unpack(self.packed_rebalancing_params) + current_rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params) if new_allowed_extra_profit > 10**18: new_allowed_extra_profit = current_rebalancing_params[0] @@ -1974,7 +1988,7 @@ def apply_new_parameters( else: new_ma_time = current_rebalancing_params[2] - self.packed_rebalancing_params = self._pack( + self.packed_rebalancing_params = self._pack_3( [new_allowed_extra_profit, new_adjustment_step, new_ma_time] ) diff --git a/tests/unitary/math/test_get_y.py b/tests/unitary/math/test_get_y.py index cefbab13..789bd10e 100644 --- a/tests/unitary/math/test_get_y.py +++ b/tests/unitary/math/test_get_y.py @@ -104,12 +104,9 @@ def calculate_F_by_y0(y0): def test_get_y_revert(math_contract): a = 1723894848 gamma = 24009999997600 - x = [ - 112497148627520223862735198942112, - 112327102289152450435452075003508 - ] + x = [112497148627520223862735198942112, 112327102289152450435452075003508] D = 224824250915890636214130540882688 i = 0 - - y_out = math_contract.newton_y(a, gamma, x, D, i) - y_out = math_contract.get_y(a, gamma, x, D, i) + + math_contract.newton_y(a, gamma, x, D, i) + math_contract.get_y(a, gamma, x, D, i) diff --git a/tests/unitary/math/test_packing.py b/tests/unitary/math/test_packing.py new file mode 100644 index 00000000..e263ff63 --- /dev/null +++ b/tests/unitary/math/test_packing.py @@ -0,0 +1,29 @@ +import boa +from boa.test import strategy +from hypothesis import given, settings + + +@given(val=strategy("uint256[3]", max_value=10**18)) +@settings(max_examples=10000, deadline=None) +def test_pack_unpack_three_integers(swap, twocrypto_factory, val): + + for contract in [swap, twocrypto_factory]: + packed = contract.internal._pack_3(val) + unpacked = swap.internal._unpack_3(packed) # swap unpacks + for i in range(3): + assert unpacked[i] == val[i] + + +@given(val=strategy("uint256[2]", max_value=2**128)) +@settings(max_examples=10000, deadline=None) +def test_pack_unpack_2_integers(swap, val): + + if max(val) >= 2**128: + with boa.reverts(): + swap.internal._pack_2(val[0], val[1]) + return + + packed = swap.internal._pack_2(val[0], val[1]) + unpacked = swap.internal._unpack_2(packed) # swap unpacks + for i in range(2): + assert unpacked[i] == val[i] From 52c03cb688253085f01fa94d631311729b5cb7cc Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:29:46 +0100 Subject: [PATCH 34/72] remove asserts because packed2 values are timestamps so no need to do gas consuming checks --- contracts/main/CurveTwocryptoOptimized.vy | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 5a7ce96e..101ac0a3 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -757,8 +757,6 @@ def _unpack_3(_packed: uint256) -> uint256[3]: @pure @internal def _pack_2(p1: uint256, p2: uint256) -> uint256: - assert p1 < 2**128 - assert p2 < 2**128 return p1 | (p2 << 128) From 407fd0f75c789ac48a4c34f7419e8b6628955e58 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:43:41 +0100 Subject: [PATCH 35/72] claim fees before remove liq one --- contracts/main/CurveTwocryptoOptimized.vy | 8 ++--- tests/fixtures/accounts.py | 7 ++++ tests/unitary/math/test_packing.py | 8 +---- tests/unitary/pool/test_admin_fee.py | 40 +++++++++++++++++++++++ 4 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 tests/unitary/pool/test_admin_fee.py diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 101ac0a3..297e3fca 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -461,6 +461,8 @@ def add_liquidity( @return uint256 Amount of LP tokens received by the `receiver """ + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. + A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) @@ -564,8 +566,6 @@ def add_liquidity( price_scale ) - self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. - return d_token @@ -683,6 +683,8 @@ def remove_liquidity_one_coin( @return Amount of tokens at index i received by the `receiver` """ + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. + A_gamma: uint256[2] = self._A_gamma() dy: uint256 = 0 @@ -720,8 +722,6 @@ def remove_liquidity_one_coin( msg.sender, token_amount, i, dy, approx_fee, packed_price_scale ) - self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. - return dy diff --git a/tests/fixtures/accounts.py b/tests/fixtures/accounts.py index 278e8ca1..3099db89 100644 --- a/tests/fixtures/accounts.py +++ b/tests/fixtures/accounts.py @@ -37,6 +37,13 @@ def user(): return acc +@pytest.fixture(scope="module") +def user_b(): + acc = boa.env.generate_address() + boa.env.set_balance(acc, 10**25) + return acc + + @pytest.fixture(scope="module") def users(): accs = [i() for i in [boa.env.generate_address] * 10] diff --git a/tests/unitary/math/test_packing.py b/tests/unitary/math/test_packing.py index e263ff63..64de9215 100644 --- a/tests/unitary/math/test_packing.py +++ b/tests/unitary/math/test_packing.py @@ -1,4 +1,3 @@ -import boa from boa.test import strategy from hypothesis import given, settings @@ -14,15 +13,10 @@ def test_pack_unpack_three_integers(swap, twocrypto_factory, val): assert unpacked[i] == val[i] -@given(val=strategy("uint256[2]", max_value=2**128)) +@given(val=strategy("uint256[2]", max_value=2**128 - 1)) @settings(max_examples=10000, deadline=None) def test_pack_unpack_2_integers(swap, val): - if max(val) >= 2**128: - with boa.reverts(): - swap.internal._pack_2(val[0], val[1]) - return - packed = swap.internal._pack_2(val[0], val[1]) unpacked = swap.internal._unpack_2(packed) # swap unpacks for i in range(2): diff --git a/tests/unitary/pool/test_admin_fee.py b/tests/unitary/pool/test_admin_fee.py new file mode 100644 index 00000000..c0f4ec4c --- /dev/null +++ b/tests/unitary/pool/test_admin_fee.py @@ -0,0 +1,40 @@ +import boa + +from tests.fixtures.pool import INITIAL_PRICES +from tests.utils.tokens import mint_for_testing + + +def test_admin_fee_after_deposit(swap, coins, fee_receiver, user, user_b): + quantities = [10**42 // p for p in INITIAL_PRICES] + + for coin, q in zip(coins, quantities): + for u in [user, user_b]: + mint_for_testing(coin, u, q) + with boa.env.prank(u): + coin.approve(swap, 2**256 - 1) + + split_quantities = [quantities[0] // 100, quantities[1] // 100] + + with boa.env.prank(user): + swap.add_liquidity(split_quantities, 0) + + with boa.env.prank(user_b): + for _ in range(100): + before = coins[1].balanceOf(user_b) + swap.exchange(0, 1, split_quantities[0] // 100, 0) + after = coins[1].balanceOf(user_b) + to_swap = after - before + swap.exchange(1, 0, to_swap, 0) + + balances = [swap.balances(i) for i in range(2)] + print("Balance of the pool: " + str(balances[0]) + ", " + str(balances[1])) + + ratio = 0.0001 + split_quantities = [int(balances[0] * ratio), int(balances[1] * ratio)] + with boa.env.prank(user): + swap.add_liquidity(split_quantities, 0) + + print("FEES 0: " + str(coins[0].balanceOf(fee_receiver))) + print("FEES 1: " + str(coins[1].balanceOf(fee_receiver))) + + return swap From 91122cdd25cf39287ee4ee0e02c857a9929efb78 Mon Sep 17 00:00:00 2001 From: Filipp <> Date: Wed, 15 Nov 2023 11:58:06 +0300 Subject: [PATCH 36/72] Add multiple get_y test instances, uncomment asserts. --- contracts/experimental/n=2.vy | 4 ++-- contracts/main/CurveCryptoMathOptimized2.vy | 13 ++++++------- tests/unitary/math/test_get_y.py | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/experimental/n=2.vy b/contracts/experimental/n=2.vy index 507f8e25..d7ddbc70 100644 --- a/contracts/experimental/n=2.vy +++ b/contracts/experimental/n=2.vy @@ -6,10 +6,10 @@ PRECISION: constant(uint256) = 10 ** 18 # The precision to convert to A_MULTIPLIER: constant(uint256) = 10000 MIN_GAMMA: constant(uint256) = 10**10 -MAX_GAMMA: constant(uint256) = 2 * 10**16 +MAX_GAMMA: constant(uint256) = 2 * 10**15 MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 -MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 100000 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 @internal diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index 82a9a5fd..add9a39b 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -208,9 +208,9 @@ def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256: # Safety checks - # assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A - # assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma - # assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D + assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A + assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D y: uint256 = self._newton_y(ANN, gamma, x, D, i) frac: uint256 = y * 10**18 / D @@ -230,9 +230,9 @@ def get_y( ) -> uint256[2]: # Safety checks - # assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A - # assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma - # assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D + assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A + assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D ANN: int256 = convert(_ANN, int256) gamma: int256 = convert(_gamma, int256) @@ -354,7 +354,6 @@ def get_y( frac: uint256 = y_out[0] * 10**18 / _D assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y - # return [a, b, c, d, delta0, delta1, C1, b_cbrt, second_cbrt, D**2/x_j*root/4/10**18] return y_out diff --git a/tests/unitary/math/test_get_y.py b/tests/unitary/math/test_get_y.py index 6f0425b7..e4cc0b0a 100644 --- a/tests/unitary/math/test_get_y.py +++ b/tests/unitary/math/test_get_y.py @@ -45,7 +45,7 @@ def inv_target_decimal_n2(A, gamma, x, D): return f - +@pytest.mark.parametrize("_tmp", range(32)) # Create 32 independent test instances. @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( @@ -61,7 +61,7 @@ def inv_target_decimal_n2(A, gamma, x, D): j=st.integers(min_value=0, max_value=1), ) @settings(max_examples=MAX_SAMPLES, deadline=None) -def test_get_y(math_unoptimized, math_optimized, A, D, xD, yD, gamma, j): +def test_get_y(math_unoptimized, math_optimized, A, D, xD, yD, gamma, j, _tmp): pytest.current_case_id += 1 X = [D * xD // 10**18, D * yD // 10**18] From 92a0435546ffd1a4ea5c77539bbd37f52e03eafb Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:17:40 +0100 Subject: [PATCH 37/72] unsafe add nonce --- contracts/main/LiquidityGauge.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/LiquidityGauge.vy b/contracts/main/LiquidityGauge.vy index 87307c4d..9c02d1b3 100644 --- a/contracts/main/LiquidityGauge.vy +++ b/contracts/main/LiquidityGauge.vy @@ -572,7 +572,7 @@ def permit( assert ecrecover(digest, _v, _r, _s) == _owner # dev: invalid signature self.allowance[_owner][_spender] = _value - self.nonces[_owner] = nonce + 1 + self.nonces[_owner] = unsafe_add(nonce, 1) log Approval(_owner, _spender, _value) return True From fe68f2cb3ff2c2550ff2e7e485686796b24c39b0 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:23:34 +0100 Subject: [PATCH 38/72] reduce unnecessary storage reads in some conditions in _claim_admin_fees --- contracts/main/CurveTwocryptoOptimized.vy | 5 ++--- tests/unitary/math/test_get_y.py | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 297e3fca..d6955362 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -1080,12 +1080,9 @@ def _claim_admin_fees(): ): return - A_gamma: uint256[2] = self._A_gamma() - xcp_profit: uint256 = self.xcp_profit # <---------- Current pool profits. xcp_profit_a: uint256 = self.xcp_profit_a # <- Profits at previous claim. current_lp_token_supply: uint256 = self.totalSupply - D: uint256 = self.D # Do not claim admin fees if: # 1. insufficient profits accrued since last claim, and @@ -1097,6 +1094,8 @@ def _claim_admin_fees(): # ---------- Conditions met to claim admin fees: compute state. ---------- + A_gamma: uint256[2] = self._A_gamma() + D: uint256 = self.D vprice: uint256 = self.virtual_price price_scale: uint256 = self.cached_price_scale fee_receiver: address = factory.fee_receiver() diff --git a/tests/unitary/math/test_get_y.py b/tests/unitary/math/test_get_y.py index e870e364..196c7803 100644 --- a/tests/unitary/math/test_get_y.py +++ b/tests/unitary/math/test_get_y.py @@ -45,7 +45,10 @@ def inv_target_decimal_n2(A, gamma, x, D): return f -@pytest.mark.parametrize("_tmp", range(32)) # Create 32 independent test instances. + +@pytest.mark.parametrize( + "_tmp", range(32) +) # Create 32 independent test instances. @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( From 6fce03ca0217d06e072391fb1a92b9147666d16a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:32:57 +0100 Subject: [PATCH 39/72] use unsafe sub for +ve timedeltas --- contracts/main/CurveTwocryptoOptimized.vy | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index d6955362..36e6fbf5 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -645,7 +645,7 @@ def remove_liquidity( alpha: uint256 = MATH.wad_exp( -convert( unsafe_div( - (block.timestamp - last_timestamp[1]) * 10**18, + unsafe_sub(block.timestamp, last_timestamp[1]) * 10**18, self.xcp_ma_time # <---------- xcp ma time has is longer. ), int256, @@ -883,7 +883,7 @@ def tweak_price( alpha = MATH.wad_exp( -convert( unsafe_div( - (block.timestamp - last_timestamp[0]) * 10**18, + unsafe_sub(block.timestamp, last_timestamp[0]) * 10**18, rebalancing_params[2] # <----------------------- ma_time. ), int256, @@ -911,7 +911,7 @@ def tweak_price( alpha = MATH.wad_exp( -convert( unsafe_div( - (block.timestamp - last_timestamp[1]) * 10**18, + unsafe_sub(block.timestamp, last_timestamp[1]) * 10**18, self.xcp_ma_time # <---------- xcp ma time has is longer. ), int256, @@ -1075,7 +1075,7 @@ def _claim_admin_fees(): last_claim_time: uint256 = self.last_admin_fee_claim_timestamp if ( - block.timestamp - last_claim_time < MIN_ADMIN_FEE_CLAIM_INTERVAL or + unsafe_sub(block.timestamp, last_claim_time) < MIN_ADMIN_FEE_CLAIM_INTERVAL or self.future_A_gamma_time > block.timestamp ): return @@ -1528,7 +1528,7 @@ def internal_price_oracle() -> uint256: ma_time: uint256 = self._unpack_3(self.packed_rebalancing_params)[2] alpha: uint256 = MATH.wad_exp( -convert( - (block.timestamp - last_prices_timestamp) * 10**18 / ma_time, + unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18 / ma_time, int256, ) ) @@ -1662,7 +1662,10 @@ def xcp_oracle() -> uint256: alpha: uint256 = MATH.wad_exp( -convert( - (block.timestamp - last_prices_timestamp) * 10**18 / self.xcp_ma_time, + unsafe_div( + unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18, + self.xcp_ma_time + ), int256, ) ) From f4ab10862436a851656cf102d7e97856f94d6fa3 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:38:02 +0100 Subject: [PATCH 40/72] gas: remove unnecessary re-read of storage in remove_liquidity --- contracts/main/CurveTwocryptoOptimized.vy | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 36e6fbf5..4ea2f86d 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -636,7 +636,7 @@ def remove_liquidity( # Update xcp since liquidity was removed: xp: uint256[N_COINS] = self.xp(self.balances, self.cached_price_scale) - self.last_xcp = isqrt(xp[0] * xp[1]) + last_xcp: uint256 = isqrt(xp[0] * xp[1]) # <----------- Cache it for now. last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) if last_timestamp[1] < block.timestamp: @@ -653,7 +653,7 @@ def remove_liquidity( ) self.cached_xcp_oracle = unsafe_div( - self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, + last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, 10**18 ) last_timestamp[1] = block.timestamp @@ -661,6 +661,9 @@ def remove_liquidity( # Pack and store timestamps: self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) + # Store last xcp + self.last_xcp = last_xcp + return withdraw_amounts From eb4acb31fdff8a1364b70149cd83bf485548c913 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:21:09 +0100 Subject: [PATCH 41/72] add fuzz to this test --- contracts/main/CurveTwocryptoOptimized.vy | 4 +++- tests/unitary/pool/test_admin_fee.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 4ea2f86d..8c8263b3 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -461,7 +461,7 @@ def add_liquidity( @return uint256 Amount of LP tokens received by the `receiver """ - self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. + # self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances @@ -566,6 +566,8 @@ def add_liquidity( price_scale ) + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. + return d_token diff --git a/tests/unitary/pool/test_admin_fee.py b/tests/unitary/pool/test_admin_fee.py index c0f4ec4c..91c644ee 100644 --- a/tests/unitary/pool/test_admin_fee.py +++ b/tests/unitary/pool/test_admin_fee.py @@ -1,10 +1,16 @@ import boa +from hypothesis import example, given, settings +from hypothesis import strategies as st from tests.fixtures.pool import INITIAL_PRICES from tests.utils.tokens import mint_for_testing -def test_admin_fee_after_deposit(swap, coins, fee_receiver, user, user_b): +@given(ratio=st.floats(min_value=0.0001, max_value=0.1)) +@settings(max_examples=1000, deadline=None) +def test_admin_fee_after_deposit( + swap, coins, fee_receiver, user, user_b, ratio +): quantities = [10**42 // p for p in INITIAL_PRICES] for coin, q in zip(coins, quantities): @@ -28,8 +34,7 @@ def test_admin_fee_after_deposit(swap, coins, fee_receiver, user, user_b): balances = [swap.balances(i) for i in range(2)] print("Balance of the pool: " + str(balances[0]) + ", " + str(balances[1])) - - ratio = 0.0001 + print("Ratio:", ratio) split_quantities = [int(balances[0] * ratio), int(balances[1] * ratio)] with boa.env.prank(user): swap.add_liquidity(split_quantities, 0) @@ -37,4 +42,6 @@ def test_admin_fee_after_deposit(swap, coins, fee_receiver, user, user_b): print("FEES 0: " + str(coins[0].balanceOf(fee_receiver))) print("FEES 1: " + str(coins[1].balanceOf(fee_receiver))) + print() + return swap From 616a27836212fe476dc6f6814850e15a0f5128ab Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:21:34 +0100 Subject: [PATCH 42/72] claim fees before deposit --- contracts/main/CurveTwocryptoOptimized.vy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 8c8263b3..4ea2f86d 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -461,7 +461,7 @@ def add_liquidity( @return uint256 Amount of LP tokens received by the `receiver """ - # self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances @@ -566,8 +566,6 @@ def add_liquidity( price_scale ) - self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. - return d_token From ec5b1229fd8bbd96096e5a5163ab148e03e8f8d5 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:46:57 +0100 Subject: [PATCH 43/72] remove double mul of precisions[0] --- contracts/main/CurveCryptoViews2Optimized.vy | 4 ++-- tests/unitary/pool/test_admin_fee.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/main/CurveCryptoViews2Optimized.vy b/contracts/main/CurveCryptoViews2Optimized.vy index 145fb1e8..79f52253 100644 --- a/contracts/main/CurveCryptoViews2Optimized.vy +++ b/contracts/main/CurveCryptoViews2Optimized.vy @@ -286,11 +286,11 @@ def _calc_dtoken_nofee( amountsp[0] *= precisions[0] xp = [ - xp[0] * precisions[0], + xp[0], xp[1] * price_scale * precisions[1] / PRECISION ] amountsp = [ - amountsp[0] * precisions[0], + amountsp[0], amountsp[1] * price_scale * precisions[1] / PRECISION ] diff --git a/tests/unitary/pool/test_admin_fee.py b/tests/unitary/pool/test_admin_fee.py index 91c644ee..9702db60 100644 --- a/tests/unitary/pool/test_admin_fee.py +++ b/tests/unitary/pool/test_admin_fee.py @@ -1,5 +1,5 @@ import boa -from hypothesis import example, given, settings +from hypothesis import given, settings from hypothesis import strategies as st from tests.fixtures.pool import INITIAL_PRICES From 6d80da03af9a20fe4ed94803f973a1bf298b66d3 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:48:31 +0100 Subject: [PATCH 44/72] fix: remove double mul correctly :P --- contracts/main/CurveCryptoViews2Optimized.vy | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/main/CurveCryptoViews2Optimized.vy b/contracts/main/CurveCryptoViews2Optimized.vy index 79f52253..73c3b8c3 100644 --- a/contracts/main/CurveCryptoViews2Optimized.vy +++ b/contracts/main/CurveCryptoViews2Optimized.vy @@ -283,14 +283,12 @@ def _calc_dtoken_nofee( for k in range(N_COINS): xp[k] -= amounts[k] - amountsp[0] *= precisions[0] - xp = [ - xp[0], + xp[0] * precisions[0], xp[1] * price_scale * precisions[1] / PRECISION ] amountsp = [ - amountsp[0], + amountsp[0]* precisions[0], amountsp[1] * price_scale * precisions[1] / PRECISION ] From ba1dc63284bd5404f426a92892f35415b28c6be9 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:55:39 +0100 Subject: [PATCH 45/72] remove call to reduction coefficient --- contracts/main/CurveCryptoViews2Optimized.vy | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/main/CurveCryptoViews2Optimized.vy b/contracts/main/CurveCryptoViews2Optimized.vy index 73c3b8c3..f68266bf 100644 --- a/contracts/main/CurveCryptoViews2Optimized.vy +++ b/contracts/main/CurveCryptoViews2Optimized.vy @@ -50,9 +50,6 @@ interface Math: D: uint256, i: uint256, ) -> uint256: view - def reduction_coefficient( - x: uint256[N_COINS], fee_gamma: uint256 - ) -> uint256: view N_COINS: constant(uint256) = 2 @@ -360,10 +357,15 @@ def _calc_withdraw_one_coin( @internal @view def _fee(xp: uint256[N_COINS], swap: address) -> uint256: - math: Math = Curve(swap).MATH() + packed_fee_params: uint256 = Curve(swap).packed_fee_params() fee_params: uint256[3] = self._unpack(packed_fee_params) - f: uint256 = math.reduction_coefficient(xp, fee_params[2]) + f: uint256 = xp[0] + xp[1] + f = fee_params[2] * 10**18 / ( + fee_params[2] + 10**18 - + (10**18 * N_COINS**N_COINS) * xp[0] / f * xp[1] / f + ) + return (fee_params[0] * f + fee_params[1] * (10**18 - f)) / 10**18 From a3388cd92222e997f9437e1720a9045eaa10f58a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:00:35 +0100 Subject: [PATCH 46/72] fix: remove double precision division --- contracts/main/CurveCryptoViews2Optimized.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/CurveCryptoViews2Optimized.vy b/contracts/main/CurveCryptoViews2Optimized.vy index f68266bf..b3ad8238 100644 --- a/contracts/main/CurveCryptoViews2Optimized.vy +++ b/contracts/main/CurveCryptoViews2Optimized.vy @@ -209,7 +209,7 @@ def _get_dx_fee( xp[i] = x_out[0] if i > 0: - dx = dy * PRECISION / (price_scale * precisions[1]) + dx = dx * PRECISION / price_scale dx /= precisions[i] return dx, xp From d5005856802aeeff02bf8758f3763ec92e4319d2 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:05:00 +0100 Subject: [PATCH 47/72] fix: use correct method_id --- contracts/main/CurveTwocryptoFactoryHandler.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/CurveTwocryptoFactoryHandler.vy b/contracts/main/CurveTwocryptoFactoryHandler.vy index e949ab50..ea8f580e 100644 --- a/contracts/main/CurveTwocryptoFactoryHandler.vy +++ b/contracts/main/CurveTwocryptoFactoryHandler.vy @@ -119,7 +119,7 @@ def _get_gauge_type(_gauge: address) -> int128: success, response = raw_call( GAUGE_CONTROLLER, concat( - method_id("gauge_type(address)"), + method_id("gauge_types(address)"), convert(_gauge, bytes32), ), max_outsize=32, From 7cabdbcd257d12dfed53279b17912b24111ab01c Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:07:40 +0100 Subject: [PATCH 48/72] use correct method --- contracts/main/CurveTwocryptoFactoryHandler.vy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/main/CurveTwocryptoFactoryHandler.vy b/contracts/main/CurveTwocryptoFactoryHandler.vy index ea8f580e..615823ff 100644 --- a/contracts/main/CurveTwocryptoFactoryHandler.vy +++ b/contracts/main/CurveTwocryptoFactoryHandler.vy @@ -20,7 +20,7 @@ interface BaseRegistry: interface CurvePool: def adjustment_step() -> uint256: view - def admin_fee() -> uint256: view + def ADMIN_FEE() -> uint256: view def allowed_extra_profit() -> uint256: view def A() -> uint256: view def balances(i: uint256) -> uint256: view @@ -170,7 +170,7 @@ def get_admin_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: xcp_profit: uint256 = CurvePool(_pool).xcp_profit() xcp_profit_a: uint256 = CurvePool(_pool).xcp_profit_a() - admin_fee: uint256 = CurvePool(_pool).admin_fee() + admin_fee: uint256 = CurvePool(_pool).ADMIN_FEE() admin_balances: uint256[MAX_METAREGISTRY_COINS] = empty(uint256[MAX_METAREGISTRY_COINS]) # admin balances are non zero if pool has made more than allowed profits: @@ -275,7 +275,7 @@ def get_fees(_pool: address) -> uint256[10]: fees: uint256[10] = empty(uint256[10]) pool_fees: uint256[4] = [ CurvePool(_pool).fee(), - CurvePool(_pool).admin_fee(), + CurvePool(_pool).ADMIN_FEE(), CurvePool(_pool).mid_fee(), CurvePool(_pool).out_fee() ] From fd2e50569a67dc76ff2e1b115b438c1b9e7c37ee Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:08:42 +0100 Subject: [PATCH 49/72] call ma_time instead of ma_half_time' --- contracts/main/CurveTwocryptoFactoryHandler.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/main/CurveTwocryptoFactoryHandler.vy b/contracts/main/CurveTwocryptoFactoryHandler.vy index 615823ff..ffb200cc 100644 --- a/contracts/main/CurveTwocryptoFactoryHandler.vy +++ b/contracts/main/CurveTwocryptoFactoryHandler.vy @@ -29,7 +29,7 @@ interface CurvePool: def fee_gamma() -> uint256: view def gamma() -> uint256: view def get_virtual_price() -> uint256: view - def ma_half_time() -> uint256: view + def ma_time() -> uint256: view def mid_fee() -> uint256: view def out_fee() -> uint256: view def virtual_price() -> uint256: view @@ -390,7 +390,7 @@ def get_pool_params(_pool: address) -> uint256[20]: pool_params[3] = CurvePool(_pool).allowed_extra_profit() pool_params[4] = CurvePool(_pool).fee_gamma() pool_params[5] = CurvePool(_pool).adjustment_step() - pool_params[6] = CurvePool(_pool).ma_half_time() + pool_params[6] = CurvePool(_pool).ma_time() return pool_params From d8021ec89e0ac677d072709e8f36504c8cab5b32 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:28:40 +0100 Subject: [PATCH 50/72] check A and gamma in implementation constructor and not in factory to accommodate for future updates --- contracts/main/CurveTwocryptoFactory.vy | 13 ------------- contracts/main/CurveTwocryptoOptimized.vy | 12 +++++++++++- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/contracts/main/CurveTwocryptoFactory.vy b/contracts/main/CurveTwocryptoFactory.vy index 7a012b9a..78363ebb 100644 --- a/contracts/main/CurveTwocryptoFactory.vy +++ b/contracts/main/CurveTwocryptoFactory.vy @@ -72,12 +72,6 @@ A_MULTIPLIER: constant(uint256) = 10000 # Limits MAX_FEE: constant(uint256) = 10 * 10 ** 9 -MIN_GAMMA: constant(uint256) = 10 ** 10 -MAX_GAMMA: constant(uint256) = 5 * 10**16 - -MIN_A: constant(uint256) = N_COINS ** N_COINS * A_MULTIPLIER / 100 -MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS - admin: public(address) future_admin: public(address) @@ -150,13 +144,6 @@ def deploy_pool( pool_implementation: address = self.pool_implementations[implementation_id] assert pool_implementation != empty(address), "Pool implementation not set" - # Validate parameters - assert A > MIN_A-1 - assert A < MAX_A+1 - - assert gamma > MIN_GAMMA-1 - assert gamma < MAX_GAMMA+1 - assert mid_fee < MAX_FEE-1 # mid_fee can be zero assert out_fee >= mid_fee assert out_fee < MAX_FEE-1 diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 4ea2f86d..2923dc02 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -234,8 +234,18 @@ def __init__( PRECISIONS = precisions # <------------------------ Precisions of coins. - self.initial_A_gamma = packed_A_gamma # <------------------- A and gamma. + # --------------- Validate A and gamma parameters here and not in factory. + A_gamma: uint256[2] = self._unpack_2(packed_A_gamma) + + assert A_gamma[0] > MIN_A-1 + assert A_gamma[0] < MAX_A+1 + + assert A_gamma[1] > MIN_GAMMA-1 + assert A_gamma[1] < MAX_GAMMA+1 + + self.initial_A_gamma = packed_A_gamma self.future_A_gamma = packed_A_gamma + # ------------------------------------------------------------------------ self.packed_rebalancing_params = packed_rebalancing_params # <-- Contains # rebalancing params: allowed_extra_profit, adjustment_step, From c81dbdc93e517cbde75f2ba4ff34ec78bba9b619 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:29:55 +0100 Subject: [PATCH 51/72] use DynArrays and save a bunch of gas while deploying pool --- contracts/main/CurveTwocryptoFactory.vy | 29 ++++++++++++++----------- tests/unitary/pool/test_admin_fee.py | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/contracts/main/CurveTwocryptoFactory.vy b/contracts/main/CurveTwocryptoFactory.vy index 78363ebb..9e787dfc 100644 --- a/contracts/main/CurveTwocryptoFactory.vy +++ b/contracts/main/CurveTwocryptoFactory.vy @@ -86,12 +86,9 @@ math_implementation: public(address) # mapping of coins -> pools for trading # a mapping key is generated for each pair of addresses via # `bitwise_xor(convert(a, uint256), convert(b, uint256))` -markets: HashMap[uint256, address[4294967296]] -market_counts: HashMap[uint256, uint256] - -pool_count: public(uint256) # actual length of pool_list +markets: HashMap[uint256, DynArray[address, 4294967296]] pool_data: HashMap[address, PoolArray] -pool_list: public(address[4294967296]) # master list of pools +pool_list: public(DynArray[address, 4294967296]) # master list of pools @external @@ -203,9 +200,8 @@ def deploy_pool( ) # populate pool data - length: uint256 = self.pool_count - self.pool_list[length] = pool - self.pool_count = length + 1 + self.pool_list.append(pool) + self.pool_data[pool].decimals = decimals self.pool_data[pool].coins = _coins self.pool_data[pool].implementation = pool_implementation @@ -237,10 +233,7 @@ def _add_coins_to_market(coin_a: address, coin_b: address, pool: address): key: uint256 = ( convert(coin_a, uint256) ^ convert(coin_b, uint256) ) - - length: uint256 = self.market_counts[key] - self.markets[key][length] = pool - self.market_counts[key] = length + 1 + self.markets[key].append(pool) @external @@ -378,6 +371,16 @@ def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address # <--- Pool Getters ---> +@view +@external +def pool_count() -> uint256: + """ + @notice Get number of pools deployed from the factory + @return Number of pools deployed from factory + """ + return len(self.pool_list) + + @view @external def get_coins(_pool: address) -> address[N_COINS]: @@ -460,4 +463,4 @@ def get_market_counts(coin_a: address, coin_b: address) -> uint256: convert(coin_a, uint256) ^ convert(coin_b, uint256) ) - return self.market_counts[key] + return len(self.markets[key]) diff --git a/tests/unitary/pool/test_admin_fee.py b/tests/unitary/pool/test_admin_fee.py index 9702db60..b6bef490 100644 --- a/tests/unitary/pool/test_admin_fee.py +++ b/tests/unitary/pool/test_admin_fee.py @@ -7,7 +7,7 @@ @given(ratio=st.floats(min_value=0.0001, max_value=0.1)) -@settings(max_examples=1000, deadline=None) +@settings(max_examples=10, deadline=None) def test_admin_fee_after_deposit( swap, coins, fee_receiver, user, user_b, ratio ): From 5ae137d05d49f46ef6413b145876d1f8420403a3 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:41:54 +0100 Subject: [PATCH 52/72] use immutables --- contracts/main/LiquidityGauge.vy | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/main/LiquidityGauge.vy b/contracts/main/LiquidityGauge.vy index 9c02d1b3..1b5cd8b6 100644 --- a/contracts/main/LiquidityGauge.vy +++ b/contracts/main/LiquidityGauge.vy @@ -112,16 +112,16 @@ balanceOf: public(HashMap[address, uint256]) totalSupply: public(uint256) allowance: public(HashMap[address, HashMap[address, uint256]]) -name: public(String[64]) -symbol: public(String[40]) +name: public(immutable(String[64])) +symbol: public(immutable(String[40])) # ERC2612 nonces: public(HashMap[address, uint256]) # Gauge -factory: public(address) +factory: public(immutable(address)) +lp_token: public(immutable(address)) manager: public(address) -lp_token: public(address) is_killed: public(bool) @@ -170,15 +170,15 @@ def __init__(_lp_token: address): @notice Contract constructor @param _lp_token Liquidity Pool contract address """ - self.lp_token = _lp_token - self.factory = msg.sender + lp_token = _lp_token + factory = msg.sender self.manager = msg.sender - symbol: String[32] = ERC20Extended(_lp_token).symbol() - name: String[64] = concat("Curve.fi ", symbol, " Gauge Deposit") + _symbol: String[32] = ERC20Extended(_lp_token).symbol() + _name: String[64] = concat("Curve.fi ", symbol, " Gauge Deposit") - self.name = name - self.symbol = concat(symbol, "-gauge") + name = _name + symbol = concat(_symbol, "-gauge") self.period_timestamp[0] = block.timestamp self.inflation_params = ( @@ -186,7 +186,7 @@ def __init__(_lp_token: address): + CRV20(CRV).rate() ) - NAME_HASH = keccak256(name) + NAME_HASH = keccak256(_name) salt = block.prevhash CACHED_CHAIN_ID = chain.id CACHED_DOMAIN_SEPARATOR = keccak256( @@ -426,7 +426,7 @@ def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = self._update_liquidity_limit(_addr, new_balance, total_supply) - ERC20(self.lp_token).transferFrom(msg.sender, self, _value) + ERC20(lp_token).transferFrom(msg.sender, self, _value) log Deposit(_addr, _value) log Transfer(empty(address), _addr, _value) @@ -455,7 +455,7 @@ def withdraw(_value: uint256, _claim_rewards: bool = False): self._update_liquidity_limit(msg.sender, new_balance, total_supply) - ERC20(self.lp_token).transfer(msg.sender, _value) + ERC20(lp_token).transfer(msg.sender, _value) log Withdraw(msg.sender, _value) log Transfer(msg.sender, empty(address), _value) @@ -669,7 +669,7 @@ def set_gauge_manager(_gauge_manager: address): method, but only for the gauge which they are the manager of. @param _gauge_manager The account to set as the new manager of the gauge. """ - assert msg.sender in [self.manager, Factory(self.factory).admin()] # dev: only manager or factory admin + assert msg.sender in [self.manager, Factory(factory).admin()] # dev: only manager or factory admin self.manager = _gauge_manager log SetGaugeManager(_gauge_manager) @@ -719,7 +719,7 @@ def add_reward(_reward_token: address, _distributor: address): @param _reward_token The token to add as an additional reward @param _distributor Address permitted to fund this contract with the reward token """ - assert msg.sender in [self.manager, Factory(self.factory).admin()] # dev: only manager or factory admin + assert msg.sender in [self.manager, Factory(factory).admin()] # dev: only manager or factory admin assert _distributor != empty(address) # dev: distributor cannot be zero address reward_count: uint256 = self.reward_count @@ -740,7 +740,7 @@ def set_reward_distributor(_reward_token: address, _distributor: address): """ current_distributor: address = self.reward_data[_reward_token].distributor - assert msg.sender in [current_distributor, Factory(self.factory).admin(), self.manager] + assert msg.sender in [current_distributor, Factory(factory).admin(), self.manager] assert current_distributor != empty(address) assert _distributor != empty(address) @@ -754,7 +754,7 @@ def set_killed(_is_killed: bool): @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV @param _is_killed Killed status to set """ - assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert msg.sender == Factory(factory).admin() # dev: only owner self.is_killed = _is_killed From f9958d3f937456090eeac5636088a5a269c4c04f Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:45:05 +0100 Subject: [PATCH 53/72] log new approval event --- contracts/main/LiquidityGauge.vy | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/main/LiquidityGauge.vy b/contracts/main/LiquidityGauge.vy index 1b5cd8b6..dbc0a358 100644 --- a/contracts/main/LiquidityGauge.vy +++ b/contracts/main/LiquidityGauge.vy @@ -478,7 +478,7 @@ def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(addres @external @nonreentrant('lock') -def transferFrom(_from: address, _to :address, _value: uint256) -> bool: +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: """ @notice Transfer tokens from one address to another. @dev Transferring claims pending reward tokens for the sender and receiver @@ -488,7 +488,9 @@ def transferFrom(_from: address, _to :address, _value: uint256) -> bool: """ _allowance: uint256 = self.allowance[_from][msg.sender] if _allowance != max_value(uint256): - self.allowance[_from][msg.sender] = _allowance - _value + _new_allowance: uint256 = _allowance - _value + self.allowance[_from][msg.sender] = _new_allowance + log Approval(_from, msg.sender, _new_allowance) self._transfer(_from, _to, _value) From 4a95cc10ad8db220ccbd81ca3ed1a15d47dcba0a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:40:30 +0100 Subject: [PATCH 54/72] unsafe divs and muls in get_y --- contracts/main/CurveCryptoMathOptimized2.vy | 39 ++++++++++++--------- contracts/main/CurveTwocryptoOptimized.vy | 4 +-- tests/unitary/math/test_get_y.py | 31 +++++++++------- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index add9a39b..0bd1a337 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -240,37 +240,42 @@ def get_y( x_j: int256 = convert(_x[1 - i], int256) gamma2: int256 = unsafe_mul(gamma, gamma) + # savediv by x_j done here: y: int256 = D**2 / (x_j * N_COINS**2) - K0_i: int256 = (10**18 * N_COINS) * x_j / D - assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i] + + # K0_i: int256 = (10**18 * N_COINS) * x_j / D + K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D) + assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i] + + ann_gamma2: int256 = ANN * gamma2 # a = 10**36 / N_COINS**2 a: int256 = 10**32 # b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14 b: int256 = ( - ANN*D*gamma2/4/10000/x_j/10**4 + D*ann_gamma2/400000000/x_j - convert(unsafe_mul(10**32, 3), int256) - unsafe_mul(unsafe_mul(2, gamma), 10**14) ) # c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4 c: int256 = ( - 10**32*3 - + 4*gamma*10**14 - + gamma2/10**4 - + 4*ANN*gamma2/10000/4/10**4*x_j/D - - 4*ANN*gamma2/10000/4/10**4 + unsafe_mul(10**32, convert(3, int256)) + + unsafe_mul(unsafe_mul(4, gamma), 10**14) + + unsafe_div(gamma2, 10**4) + + unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D) + - unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) ) # d = -(10**18+gamma)**2 / 10**4 - d: int256 = -(10**18+gamma)**2 / 10**4 + d: int256 = -unsafe_div(unsafe_add(10**18, gamma) ** 2, 10**4) # delta0: int256 = 3*a*c/b - b - delta0: int256 = 3*a*c/b - b + delta0: int256 = 3 * a * c / b - b # safediv by b # delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b - delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b + delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b divider: int256 = 1 threshold: int256 = min(min(abs(delta0), abs(delta1)), a) @@ -308,11 +313,11 @@ def get_y( c = unsafe_div(c, divider) d = unsafe_div(d, divider) - # delta0 = 3*a*c/b - b - delta0 = 3*a*c/b - b + # delta0 = 3*a*c/b - b: here we can do more unsafe ops now: + delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b # delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b - delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b + delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b) # sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0 sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0) @@ -345,13 +350,13 @@ def get_y( # root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here. root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a) - # return [ + # y_out: uint256[2] = [ # convert(D**2/x_j*root/4/10**18, uint256), # <--- y # convert(root, uint256) # <----------------------- K0Prev # ] y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)] - frac: uint256 = y_out[0] * 10**18 / _D + frac: uint256 = unsafe_div(y_out[0] * 10**18, _D) assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y return y_out @@ -376,7 +381,7 @@ def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev x = [x_unsorted[1], x_unsorted[0]] assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0] - assert x[1] * 10**18 / x[0] > 10**14-1 # dev: unsafe values x[i] (input) + assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input) S: uint256 = x[0] + x[1] diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 2923dc02..99bd487c 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -505,11 +505,11 @@ def add_liquidity( xp = [ xp[0] * PRECISIONS[0], - xp[1] * price_scale * PRECISIONS[1] / PRECISION + unsafe_div(xp[1] * price_scale * PRECISIONS[1], PRECISION) ] xp_old = [ xp_old[0] * PRECISIONS[0], - xp_old[1] * price_scale * PRECISIONS[1] / PRECISION + unsafe_div(xp_old[1] * price_scale * PRECISIONS[1], PRECISION) ] for i in range(N_COINS): diff --git a/tests/unitary/math/test_get_y.py b/tests/unitary/math/test_get_y.py index 196c7803..f923df84 100644 --- a/tests/unitary/math/test_get_y.py +++ b/tests/unitary/math/test_get_y.py @@ -2,12 +2,14 @@ import time from decimal import Decimal +import boa import pytest from hypothesis import given, note, settings from hypothesis import strategies as st N_COINS = 2 MAX_SAMPLES = 1000000 # Increase for fuzzing +N_CASES = 32 A_MUL = 10000 MIN_A = int(N_COINS**N_COINS * A_MUL / 10) @@ -46,9 +48,23 @@ def inv_target_decimal_n2(A, gamma, x, D): return f +def test_get_y_revert(math_contract): + a = 1723894848 + gamma = 24009999997600 + x = [112497148627520223862735198942112, 112327102289152450435452075003508] + D = 224824250915890636214130540882688 + i = 0 + + with boa.reverts(): + math_contract.newton_y(a, gamma, x, D, i) + + with boa.reverts(): + math_contract.get_y(a, gamma, x, D, i) + + @pytest.mark.parametrize( - "_tmp", range(32) -) # Create 32 independent test instances. + "_tmp", range(N_CASES) +) # Create N_CASES independent test instances. @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( @@ -102,14 +118,3 @@ def calculate_F_by_y0(y0): ) or abs(calculate_F_by_y0(result_get_y)) <= abs( calculate_F_by_y0(result_original) ) - - -def test_get_y_revert(math_contract): - a = 1723894848 - gamma = 24009999997600 - x = [112497148627520223862735198942112, 112327102289152450435452075003508] - D = 224824250915890636214130540882688 - i = 0 - - math_contract.newton_y(a, gamma, x, D, i) - math_contract.get_y(a, gamma, x, D, i) From 0692ba9ce30c21acb90a21caacee04fbd374d7e3 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:44:14 +0100 Subject: [PATCH 55/72] add one unsafe add --- contracts/main/CurveCryptoMathOptimized2.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index 0bd1a337..9720c431 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -383,7 +383,7 @@ def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0] assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input) - S: uint256 = x[0] + x[1] + S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds D: uint256 = 0 if K0_prev == 0: From ac588b0a37d7aeb119649798f7b96fe104bc9e12 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:15:48 +0100 Subject: [PATCH 56/72] gas: newton_D --- contracts/main/CurveCryptoMathOptimized2.vy | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index 9720c431..aa67003c 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -395,11 +395,12 @@ def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev D = S __g1k0: uint256 = gamma + 10**18 + diff: uint256 = 0 for i in range(255): D_prev: uint256 = D assert D > 0 - # Unsafe ivision by D is now safe + # Unsafe division by D and D_prev is now safe # K0: uint256 = 10**18 # for _x in x: @@ -409,9 +410,9 @@ def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev _g1k0: uint256 = __g1k0 if _g1k0 > K0: - _g1k0 = unsafe_sub(_g1k0, K0) + 1 # > 0 + _g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0 else: - _g1k0 = unsafe_sub(K0, _g1k0) + 1 # > 0 + _g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0 # D / (A * N**N) * _g1k0**2 / gamma**2 mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN) @@ -419,22 +420,22 @@ def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev # 2*N*K0 / _g1k0 mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0) + # calculate neg_fprime. here K0 > 0 is being validated (safediv). neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18) - # D -= f / fprime + # D -= f / fprime; neg_fprime safediv being validated D_plus: uint256 = D * (neg_fprime + S) / neg_fprime - D_minus: uint256 = D*D / neg_fprime + D_minus: uint256 = unsafe_div(D * D, neg_fprime) if 10**18 > K0: - D_minus += unsafe_div(D * (mul1 / neg_fprime), 10**18) * unsafe_sub(10**18, K0) / K0 + D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0) else: - D_minus -= unsafe_div(D * (mul1 / neg_fprime), 10**18) * unsafe_sub(K0, 10**18) / K0 + D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0) if D_plus > D_minus: D = unsafe_sub(D_plus, D_minus) else: D = unsafe_div(unsafe_sub(D_minus, D_plus), 2) - diff: uint256 = 0 if D > D_prev: diff = unsafe_sub(D, D_prev) else: From 7875cae1bfc99abc090e5badf9252766df2cce7a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 23 Nov 2023 20:07:46 +0100 Subject: [PATCH 57/72] check nonzero math impl --- contracts/main/CurveCryptoMathOptimized2.vy | 1 - contracts/main/CurveTwocryptoFactory.vy | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index aa67003c..29c1ad20 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -496,7 +496,6 @@ def get_p( denominator: uint256 = (GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[0], _D) * K0, 10**36) ) # p_xy = x * (GK0 + NNAG2 * y / D * K0 / 10**36) / y * 10**18 / denominator - # p_xz = x * (GK0 + NNAG2 * z / D * K0 / 10**36) / z * 10**18 / denominator # p is in 10**18 precision. return unsafe_div( _xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[1], _D) * K0, 10**36) ) / _xp[1] * 10**18, diff --git a/contracts/main/CurveTwocryptoFactory.vy b/contracts/main/CurveTwocryptoFactory.vy index 9e787dfc..e82eaeb6 100644 --- a/contracts/main/CurveTwocryptoFactory.vy +++ b/contracts/main/CurveTwocryptoFactory.vy @@ -139,7 +139,9 @@ def deploy_pool( @return Address of the deployed pool """ pool_implementation: address = self.pool_implementations[implementation_id] + _math_implementation: address = self.math_implementation assert pool_implementation != empty(address), "Pool implementation not set" + assert _math_implementation != empty(address), "Math implementation not set" assert mid_fee < MAX_FEE-1 # mid_fee can be zero assert out_fee >= mid_fee @@ -183,7 +185,6 @@ def deploy_pool( # pool is an ERC20 implementation _salt: bytes32 = block.prevhash - _math_implementation: address = self.math_implementation pool: address = create_from_blueprint( pool_implementation, _name, From 9487057282539456cc21dd31bafecf3508663a36 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 23 Nov 2023 20:11:00 +0100 Subject: [PATCH 58/72] add salt to eip712 tyephash --- contracts/main/LiquidityGauge.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/LiquidityGauge.vy b/contracts/main/LiquidityGauge.vy index dbc0a358..c30fd36f 100644 --- a/contracts/main/LiquidityGauge.vy +++ b/contracts/main/LiquidityGauge.vy @@ -91,7 +91,7 @@ WEEK: constant(uint256) = 604800 VERSION: constant(String[8]) = "v6.1.0" # <- updated from v6.0.0 (makes rewards semi-permissionless) -EIP712_TYPEHASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") +EIP712_TYPEHASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)") EIP2612_TYPEHASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") VERSION_HASH: constant(bytes32) = keccak256(VERSION) From adfed8dec631d9dec1fe208ec532eba96cab14dd Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:12:45 +0100 Subject: [PATCH 59/72] add more newton_D checks --- contracts/main/CurveCryptoMathOptimized2.vy | 6 +- contracts/old/CurveCryptoSwap2Math.vy | 193 ++++++++++++++++++++ tests/unitary/math/conftest.py | 2 +- tests/unitary/math/test_newton_D_ref.py | 190 +++++++++++++++++++ tests/utils/simulation_int_many.py | 5 + 5 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 contracts/old/CurveCryptoSwap2Math.vy create mode 100644 tests/unitary/math/test_newton_D_ref.py diff --git a/contracts/main/CurveCryptoMathOptimized2.vy b/contracts/main/CurveCryptoMathOptimized2.vy index 29c1ad20..9b155388 100644 --- a/contracts/main/CurveCryptoMathOptimized2.vy +++ b/contracts/main/CurveCryptoMathOptimized2.vy @@ -214,7 +214,7 @@ def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: u y: uint256 = self._newton_y(ANN, gamma, x, D, i) frac: uint256 = y * 10**18 / D - assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y + assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y return y @@ -357,7 +357,7 @@ def get_y( y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)] frac: uint256 = unsafe_div(y_out[0] * 10**18, _D) - assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y + assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y return y_out @@ -445,7 +445,7 @@ def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev for _x in x: frac: uint256 = _x * 10**18 / D - assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i] + assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i] return D raise "Did not converge" diff --git a/contracts/old/CurveCryptoSwap2Math.vy b/contracts/old/CurveCryptoSwap2Math.vy new file mode 100644 index 00000000..724c8d40 --- /dev/null +++ b/contracts/old/CurveCryptoSwap2Math.vy @@ -0,0 +1,193 @@ +# pragma version 0.3.10 +# pragma optimize gas + +N_COINS: constant(uint256) = 2 +A_MULTIPLIER: constant(uint256) = 10000 + +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA: constant(uint256) = 2 * 10**16 + +MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 + + +@internal +@pure +def geometric_mean(unsorted_x: uint256[N_COINS], sort: bool) -> uint256: + """ + (x[0] * x[1] * ...) ** (1/N) + """ + x: uint256[N_COINS] = unsorted_x + if sort and x[0] < x[1]: + x = [unsorted_x[1], unsorted_x[0]] + D: uint256 = x[0] + diff: uint256 = 0 + for i in range(255): + D_prev: uint256 = D + # tmp: uint256 = 10**18 + # for _x in x: + # tmp = tmp * _x / D + # D = D * ((N_COINS - 1) * 10**18 + tmp) / (N_COINS * 10**18) + # line below makes it for 2 coins + D = (D + x[0] * x[1] / D) / N_COINS + if D > D_prev: + diff = D - D_prev + else: + diff = D_prev - D + if diff <= 1 or diff * 10**18 < D: + return D + raise "Did not converge" + + +@external +@pure +def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256: + """ + Calculating x[i] given other balances x[0..N_COINS-1] and invariant D + ANN = A * N**N + """ + # Safety checks + assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A + assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D + + x_j: uint256 = x[1 - i] + y: uint256 = D**2 / (x_j * N_COINS**2) + K0_i: uint256 = (10**18 * N_COINS) * x_j / D + # S_i = x_j + + # frac = x_j * 1e18 / D => frac = K0_i / N_COINS + assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i] + + # x_sorted: uint256[N_COINS] = x + # x_sorted[i] = 0 + # x_sorted = self.sort(x_sorted) # From high to low + # x[not i] instead of x_sorted since x_soted has only 1 element + + convergence_limit: uint256 = max(max(x_j / 10**14, D / 10**14), 100) + + for j in range(255): + y_prev: uint256 = y + + K0: uint256 = K0_i * y * N_COINS / D + S: uint256 = x_j + y + + _g1k0: uint256 = gamma + 10**18 + if _g1k0 > K0: + _g1k0 = _g1k0 - K0 + 1 + else: + _g1k0 = K0 - _g1k0 + 1 + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1: uint256 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + + # 2*K0 / _g1k0 + mul2: uint256 = 10**18 + (2 * 10**18) * K0 / _g1k0 + + yfprime: uint256 = 10**18 * y + S * mul2 + mul1 + _dyfprime: uint256 = D * mul2 + if yfprime < _dyfprime: + y = y_prev / 2 + continue + else: + yfprime -= _dyfprime + fprime: uint256 = yfprime / y + + # y -= f / f_prime; y = (y * fprime - f) / fprime + # y = (yfprime + 10**18 * D - 10**18 * S) // fprime + mul1 // fprime * (10**18 - K0) // K0 + y_minus: uint256 = mul1 / fprime + y_plus: uint256 = (yfprime + 10**18 * D) / fprime + y_minus * 10**18 / K0 + y_minus += 10**18 * S / fprime + + if y_plus < y_minus: + y = y_prev / 2 + else: + y = y_plus - y_minus + + diff: uint256 = 0 + if y > y_prev: + diff = y - y_prev + else: + diff = y_prev - y + if diff < max(convergence_limit, y / 10**14): + frac: uint256 = y * 10**18 / D + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y + return y + + raise "Did not converge" + + +@external +@view +def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS]) -> uint256: + """ + Finding the invariant using Newton method. + ANN is higher by the factor A_MULTIPLIER + ANN is already A * N**N + + Currently uses 60k gas + """ + # Safety checks + assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A + assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + + # Initial value of invariant D is that for constant-product invariant + x: uint256[N_COINS] = x_unsorted + if x[0] < x[1]: + x = [x_unsorted[1], x_unsorted[0]] + + assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0] + assert x[1] * 10**18 / x[0] > 10**14 - 1 # dev: unsafe values x[i] (input) + + D: uint256 = N_COINS * self.geometric_mean(x, False) + S: uint256 = x[0] + x[1] + + for i in range(255): + D_prev: uint256 = D + + # K0: uint256 = 10**18 + # for _x in x: + # K0 = K0 * _x * N_COINS / D + # collapsed for 2 coins + K0: uint256 = (10**18 * N_COINS**2) * x[0] / D * x[1] / D + + _g1k0: uint256 = gamma + 10**18 + if _g1k0 > K0: + _g1k0 = _g1k0 - K0 + 1 + else: + _g1k0 = K0 - _g1k0 + 1 + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1: uint256 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + + # 2*N*K0 / _g1k0 + mul2: uint256 = (2 * 10**18) * N_COINS * K0 / _g1k0 + + neg_fprime: uint256 = (S + S * mul2 / 10**18) + mul1 * N_COINS / K0 - mul2 * D / 10**18 + + # D -= f / fprime + D_plus: uint256 = D * (neg_fprime + S) / neg_fprime + D_minus: uint256 = D*D / neg_fprime + if 10**18 > K0: + D_minus += D * (mul1 / neg_fprime) / 10**18 * (10**18 - K0) / K0 + else: + D_minus -= D * (mul1 / neg_fprime) / 10**18 * (K0 - 10**18) / K0 + + if D_plus > D_minus: + D = D_plus - D_minus + else: + D = (D_minus - D_plus) / 2 + + diff: uint256 = 0 + if D > D_prev: + diff = D - D_prev + else: + diff = D_prev - D + if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here + # Test that we are safe with the next newton_y + for _x in x: + frac: uint256 = _x * 10**18 / D + assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i] + return D + + raise "Did not converge" diff --git a/tests/unitary/math/conftest.py b/tests/unitary/math/conftest.py index 35e87c3e..64bcd4b4 100644 --- a/tests/unitary/math/conftest.py +++ b/tests/unitary/math/conftest.py @@ -11,4 +11,4 @@ def math_optimized(deployer): @pytest.fixture(scope="module") def math_unoptimized(deployer): with boa.env.prank(deployer): - return boa.load("contracts/experimental/n=2.vy") + return boa.load("contracts/old/CurveCryptoSwap2Math.vy") diff --git a/tests/unitary/math/test_newton_D_ref.py b/tests/unitary/math/test_newton_D_ref.py new file mode 100644 index 00000000..77ebc9d5 --- /dev/null +++ b/tests/unitary/math/test_newton_D_ref.py @@ -0,0 +1,190 @@ +# flake8: noqa +import sys +from decimal import Decimal + +from boa.vyper.contract import BoaError +from hypothesis import given, settings +from hypothesis import strategies as st + +import tests.utils.simulation_int_many as sim + +sys.stdout = sys.stderr + + +def inv_target_decimal_n2(A, gamma, x, D): + N = len(x) + + x_prod = Decimal(1) + for x_i in x: + x_prod *= x_i + K0 = x_prod / (Decimal(D) / N) ** N + K0 *= 10**18 + + if gamma > 0: + # K = gamma**2 * K0 / (gamma + 10**18*(Decimal(1) - K0))**2 + K = gamma**2 * K0 / (gamma + 10**18 - K0) ** 2 / 10**18 + K *= A + + f = ( + K * D ** (N - 1) * sum(x) + + x_prod + - (K * D**N + (Decimal(D) / N) ** N) + ) + + return f + + +N_COINS = 2 +# MAX_SAMPLES = 3000000 # Increase for fuzzing +MAX_SAMPLES = 300 # Increase for fuzzing + +A_MUL = 10000 +MIN_A = int(N_COINS**N_COINS * A_MUL / 10) +MAX_A = int(N_COINS**N_COINS * A_MUL * 1000) + +# gamma from 1e-8 up to 0.05 +MIN_GAMMA = 10**10 +MAX_GAMMA = 2 * 10**15 + +MIN_XD = 10**16 - 1 +MAX_XD = 10**20 + 1 + + +@given( + A=st.integers(min_value=MIN_A, max_value=MAX_A), + D=st.integers( + min_value=10**18, max_value=10**14 * 10**18 + ), # 1 USD to 100T USD + xD=st.integers( + min_value=int(1.001e16), max_value=int(0.999e20) + ), # <- ratio 1e18 * x/D, typically 1e18 * 1 + yD=st.integers( + min_value=int(1.001e16), max_value=int(0.999e20) + ), # <- ratio 1e18 * y/D, typically 1e18 * 1 + gamma=st.integers(min_value=MIN_GAMMA, max_value=MAX_GAMMA), + j=st.integers(min_value=0, max_value=1), + asset_x_scale_price=st.integers(min_value=10**2, max_value=10**7), + asset_y_scale_price=st.integers(min_value=10, max_value=10**5), + mid_fee=st.sampled_from( + [ + int(0.7e-3 * 10**10), + int(1e-3 * 10**10), + int(1.2e-3 * 10**10), + int(4e-3 * 10**10), + ] + ), + out_fee=st.sampled_from([int(4.0e-3 * 10**10), int(10.0e-3 * 10**10)]), + fee_gamma=st.sampled_from([int(1e-2 * 1e18), int(2e-6 * 1e18)]), +) +@settings(max_examples=MAX_SAMPLES, deadline=None) +def test_newton_D( + math_optimized, + math_unoptimized, + A, + D, + xD, + yD, + gamma, + j, + asset_x_scale_price, + asset_y_scale_price, + mid_fee, + out_fee, + fee_gamma, +): + _test_newton_D( + math_optimized, + math_unoptimized, + A, + D, + xD, + yD, + gamma, + j, + asset_x_scale_price, + asset_y_scale_price, + mid_fee, + out_fee, + fee_gamma, + ) + + +def _test_newton_D( + math_optimized, + math_unoptimized, + A, + D, + xD, + yD, + gamma, + j, + asset_x_scale_price, + asset_y_scale_price, + mid_fee, + out_fee, + fee_gamma, +): + + X = [D * xD // 10**18, D * yD // 10**18] + is_safe = all( + f >= MIN_XD and f <= MAX_XD + for f in [xx * 10**18 // D for xx in [xD, yD]] + ) + try: + newton_y_output = math_unoptimized.newton_y(A, gamma, X, D, j) + except BoaError as e: + if is_safe: + raise + else: + return + + (result_get_y, K0) = math_optimized.get_y(A, gamma, X, D, j) + + # dy should be positive + if result_get_y < X[j]: + + price_scale = (asset_x_scale_price, asset_y_scale_price) + y = X[j] + dy = X[j] - result_get_y + dy -= 1 + + if j > 0: + dy = dy * 10**18 // price_scale[j - 1] + + fee = sim.get_fee(X, fee_gamma, mid_fee, out_fee) + dy -= fee * dy // 10**10 + y -= dy + + if dy / X[j] <= 0.95: + + X[j] = y + + try: + result_sim = math_unoptimized.newton_D(A, gamma, X) + except: + breakpoint() + raise # this is a problem + + try: + result_contract = math_optimized.newton_D(A, gamma, X, K0) + except BoaError: + case = ( + "{" + f"'ANN': {A}, 'D': {D}, 'xD': {xD}, 'yD': {yD}, 'GAMMA': {gamma}, 'j': {j}, 'btcScalePrice': {btcScalePrice}, 'ethScalePrice': {ethScalePrice}, 'mid_fee': {mid_fee}, 'out_fee': {out_fee}, 'fee_gamma': {fee_gamma}" + "}" + ) + print("broken at:", case) + raise + + try: + assert abs(result_sim - result_contract) <= max( + 10000, result_sim / 1e12 + ) + except AssertionError: + case = ( + "{" + f"'ANN': {A}, 'D': {D}, 'xD': {xD}, 'yD': {yD}, 'GAMMA': {gamma}, 'j': {j}, 'btcScalePrice': {btcScalePrice}, 'ethScalePrice': {ethScalePrice}, 'mid_fee': {mid_fee}, 'out_fee': {out_fee}, 'fee_gamma': {fee_gamma}" + "},\n" + ) + print("broken at:", case) + raise diff --git a/tests/utils/simulation_int_many.py b/tests/utils/simulation_int_many.py index 179558c4..1b56ea25 100644 --- a/tests/utils/simulation_int_many.py +++ b/tests/utils/simulation_int_many.py @@ -37,6 +37,11 @@ def reduction_coefficient(x, gamma): return K +def get_fee(x, fee_gamma, mid_fee, out_fee): + f = reduction_coefficient(x, fee_gamma) + return (mid_fee * f + out_fee * (10**18 - f)) // 10**18 + + def newton_D(A, gamma, x, D0): D = D0 i = 0 From 5a038225bc36455cc6833e7c08b5149da9ad242c Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:21:27 +0100 Subject: [PATCH 60/72] fix: some errors; github workflow --- .github/workflows/.pre-commit-config.yaml | 2 +- .github/workflows/unit-tests.yaml | 2 +- tests/unitary/math/test_newton_D_ref.py | 12 ------------ 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml index 81bd81e9..3c39b1a2 100644 --- a/.github/workflows/.pre-commit-config.yaml +++ b/.github/workflows/.pre-commit-config.yaml @@ -1,6 +1,6 @@ name: pre-commit -on: [pull_request, push] +on: [push] jobs: pre-commit: diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index fefed558..d02b9553 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -1,6 +1,6 @@ name: unit-tests-boa -on: ["push", "pull_request"] +on: [push] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/tests/unitary/math/test_newton_D_ref.py b/tests/unitary/math/test_newton_D_ref.py index 77ebc9d5..5074621a 100644 --- a/tests/unitary/math/test_newton_D_ref.py +++ b/tests/unitary/math/test_newton_D_ref.py @@ -168,12 +168,6 @@ def _test_newton_D( try: result_contract = math_optimized.newton_D(A, gamma, X, K0) except BoaError: - case = ( - "{" - f"'ANN': {A}, 'D': {D}, 'xD': {xD}, 'yD': {yD}, 'GAMMA': {gamma}, 'j': {j}, 'btcScalePrice': {btcScalePrice}, 'ethScalePrice': {ethScalePrice}, 'mid_fee': {mid_fee}, 'out_fee': {out_fee}, 'fee_gamma': {fee_gamma}" - "}" - ) - print("broken at:", case) raise try: @@ -181,10 +175,4 @@ def _test_newton_D( 10000, result_sim / 1e12 ) except AssertionError: - case = ( - "{" - f"'ANN': {A}, 'D': {D}, 'xD': {xD}, 'yD': {yD}, 'GAMMA': {gamma}, 'j': {j}, 'btcScalePrice': {btcScalePrice}, 'ethScalePrice': {ethScalePrice}, 'mid_fee': {mid_fee}, 'out_fee': {out_fee}, 'fee_gamma': {fee_gamma}" - "},\n" - ) - print("broken at:", case) raise From 359951f7e14d2b2ba0fa8f8bfdc758f08dbc3b54 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:32:44 +0100 Subject: [PATCH 61/72] fix: return int instead of array of Decimal types --- tests/unitary/math/fuzz_multicoin_curve.py | 1 - tests/utils/simulation_int_many.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/unitary/math/fuzz_multicoin_curve.py b/tests/unitary/math/fuzz_multicoin_curve.py index 4575cb07..84cd3e2f 100644 --- a/tests/unitary/math/fuzz_multicoin_curve.py +++ b/tests/unitary/math/fuzz_multicoin_curve.py @@ -29,7 +29,6 @@ MIN_A = int(N_COINS**N_COINS * A_MUL / 10) MAX_A = int(N_COINS**N_COINS * A_MUL * 1000) -# gamma from 1e-8 up to 0.05 MIN_GAMMA = 10**10 MAX_GAMMA = 2 * 10**15 diff --git a/tests/utils/simulation_int_many.py b/tests/utils/simulation_int_many.py index 1b56ea25..ee6ec084 100644 --- a/tests/utils/simulation_int_many.py +++ b/tests/utils/simulation_int_many.py @@ -139,7 +139,7 @@ def newton_y(A, gamma, x, D, i): def solve_x(A, gamma, x, D, i): - return get_y_n2_dec(A, gamma, x, D, i) + return int(get_y_n2_dec(A, gamma, x, D, i)[0] * 10**18) # return newton_y(A, gamma, x, D, i) @@ -172,7 +172,10 @@ def y(self, x, i, j): xp = self.xp() xp[i] = x * self.p[i] // 10**18 yp = solve_x(self.A, self.gamma, xp, self.D(), j) - return yp * 10**18 // self.p[j] + try: + return yp * 10**18 // self.p[j] + except: + breakpoint() def get_data(fname): From e931743f0b2aebd6261feb94464769b89825dfa5 Mon Sep 17 00:00:00 2001 From: Filipp <> Date: Thu, 30 Nov 2023 17:52:17 +0300 Subject: [PATCH 62/72] Update test_newton_D_ref.py --- tests/unitary/math/test_newton_D_ref.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/unitary/math/test_newton_D_ref.py b/tests/unitary/math/test_newton_D_ref.py index 5074621a..b5a2af20 100644 --- a/tests/unitary/math/test_newton_D_ref.py +++ b/tests/unitary/math/test_newton_D_ref.py @@ -3,12 +3,13 @@ from decimal import Decimal from boa.vyper.contract import BoaError +import pytest from hypothesis import given, settings from hypothesis import strategies as st import tests.utils.simulation_int_many as sim -sys.stdout = sys.stderr +# sys.stdout = sys.stderr def inv_target_decimal_n2(A, gamma, x, D): @@ -35,8 +36,8 @@ def inv_target_decimal_n2(A, gamma, x, D): N_COINS = 2 -# MAX_SAMPLES = 3000000 # Increase for fuzzing -MAX_SAMPLES = 300 # Increase for fuzzing +MAX_SAMPLES = 3000000 # Increase for fuzzing +# MAX_SAMPLES = 300 # Increase for fuzzing A_MUL = 10000 MIN_A = int(N_COINS**N_COINS * A_MUL / 10) @@ -46,20 +47,20 @@ def inv_target_decimal_n2(A, gamma, x, D): MIN_GAMMA = 10**10 MAX_GAMMA = 2 * 10**15 -MIN_XD = 10**16 - 1 -MAX_XD = 10**20 + 1 - +MIN_XD = 10**17 +MAX_XD = 10**19 +pytest.cases = 0 @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( min_value=10**18, max_value=10**14 * 10**18 ), # 1 USD to 100T USD xD=st.integers( - min_value=int(1.001e16), max_value=int(0.999e20) + min_value=MIN_XD, max_value=MAX_XD ), # <- ratio 1e18 * x/D, typically 1e18 * 1 yD=st.integers( - min_value=int(1.001e16), max_value=int(0.999e20) + min_value=MIN_XD, max_value=MAX_XD ), # <- ratio 1e18 * y/D, typically 1e18 * 1 gamma=st.integers(min_value=MIN_GAMMA, max_value=MAX_GAMMA), j=st.integers(min_value=0, max_value=1), @@ -125,6 +126,7 @@ def _test_newton_D( fee_gamma, ): + pytest.cases += 1 X = [D * xD // 10**18, D * yD // 10**18] is_safe = all( f >= MIN_XD and f <= MAX_XD @@ -159,15 +161,17 @@ def _test_newton_D( X[j] = y + print(f'> {pytest.cases}') try: result_sim = math_unoptimized.newton_D(A, gamma, X) except: - breakpoint() + # breakpoint() raise # this is a problem try: result_contract = math_optimized.newton_D(A, gamma, X, K0) - except BoaError: + # except BoaError: + except: raise try: From cf74f173fe4b902cdb68a920b8e4b8d306401b94 Mon Sep 17 00:00:00 2001 From: Filipp Date: Mon, 4 Dec 2023 14:34:05 +0300 Subject: [PATCH 63/72] Fix newton_D_ref test params. --- tests/unitary/math/test_newton_D_ref.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/unitary/math/test_newton_D_ref.py b/tests/unitary/math/test_newton_D_ref.py index b5a2af20..489d4860 100644 --- a/tests/unitary/math/test_newton_D_ref.py +++ b/tests/unitary/math/test_newton_D_ref.py @@ -36,8 +36,9 @@ def inv_target_decimal_n2(A, gamma, x, D): N_COINS = 2 -MAX_SAMPLES = 3000000 # Increase for fuzzing +MAX_SAMPLES = 300000 # Increase for fuzzing # MAX_SAMPLES = 300 # Increase for fuzzing +N_CASES = 1 A_MUL = 10000 MIN_A = int(N_COINS**N_COINS * A_MUL / 10) @@ -51,6 +52,9 @@ def inv_target_decimal_n2(A, gamma, x, D): MAX_XD = 10**19 pytest.cases = 0 +@pytest.mark.parametrize( + "_tmp", range(N_CASES) + ) # Create N_CASES independent test instances. @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( @@ -92,6 +96,7 @@ def test_newton_D( mid_fee, out_fee, fee_gamma, + _tmp, ): _test_newton_D( math_optimized, @@ -107,6 +112,7 @@ def test_newton_D( mid_fee, out_fee, fee_gamma, + _tmp, ) @@ -124,6 +130,7 @@ def _test_newton_D( mid_fee, out_fee, fee_gamma, + _tmp, ): pytest.cases += 1 @@ -161,7 +168,8 @@ def _test_newton_D( X[j] = y - print(f'> {pytest.cases}') + # if pytest.cases % 1000 == 0: + # print(f'> {pytest.cases}') try: result_sim = math_unoptimized.newton_D(A, gamma, X) except: From def50085279cf6719f1d6923d49a5c5616542e46 Mon Sep 17 00:00:00 2001 From: Filipp <> Date: Mon, 4 Dec 2023 14:37:52 +0300 Subject: [PATCH 64/72] Fix newton_D_ref test param limits. --- tests/unitary/math/test_newton_D_ref.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/unitary/math/test_newton_D_ref.py b/tests/unitary/math/test_newton_D_ref.py index b5a2af20..489d4860 100644 --- a/tests/unitary/math/test_newton_D_ref.py +++ b/tests/unitary/math/test_newton_D_ref.py @@ -36,8 +36,9 @@ def inv_target_decimal_n2(A, gamma, x, D): N_COINS = 2 -MAX_SAMPLES = 3000000 # Increase for fuzzing +MAX_SAMPLES = 300000 # Increase for fuzzing # MAX_SAMPLES = 300 # Increase for fuzzing +N_CASES = 1 A_MUL = 10000 MIN_A = int(N_COINS**N_COINS * A_MUL / 10) @@ -51,6 +52,9 @@ def inv_target_decimal_n2(A, gamma, x, D): MAX_XD = 10**19 pytest.cases = 0 +@pytest.mark.parametrize( + "_tmp", range(N_CASES) + ) # Create N_CASES independent test instances. @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( @@ -92,6 +96,7 @@ def test_newton_D( mid_fee, out_fee, fee_gamma, + _tmp, ): _test_newton_D( math_optimized, @@ -107,6 +112,7 @@ def test_newton_D( mid_fee, out_fee, fee_gamma, + _tmp, ) @@ -124,6 +130,7 @@ def _test_newton_D( mid_fee, out_fee, fee_gamma, + _tmp, ): pytest.cases += 1 @@ -161,7 +168,8 @@ def _test_newton_D( X[j] = y - print(f'> {pytest.cases}') + # if pytest.cases % 1000 == 0: + # print(f'> {pytest.cases}') try: result_sim = math_unoptimized.newton_D(A, gamma, X) except: From 2b46e412942da2a7df8eef51a6dd0f37125b300f Mon Sep 17 00:00:00 2001 From: Filipp <> Date: Tue, 5 Dec 2023 09:48:32 +0300 Subject: [PATCH 65/72] Update newton_D test. --- tests/unitary/math/test_newton_D.py | 116 +++++++++++----------------- 1 file changed, 46 insertions(+), 70 deletions(-) diff --git a/tests/unitary/math/test_newton_D.py b/tests/unitary/math/test_newton_D.py index 0847cc5c..5128b8e8 100644 --- a/tests/unitary/math/test_newton_D.py +++ b/tests/unitary/math/test_newton_D.py @@ -2,16 +2,15 @@ import sys import time from decimal import Decimal - import pytest from boa.vyper.contract import BoaError from hypothesis import given, settings from hypothesis import strategies as st - import tests.utils.simulation_int_many as sim -sys.stdout = sys.stderr +# Uncomment to be able to print when parallelized +# sys.stdout = sys.stderr def inv_target_decimal_n2(A, gamma, x, D): N = len(x) @@ -37,7 +36,8 @@ def inv_target_decimal_n2(A, gamma, x, D): N_COINS = 2 -MAX_SAMPLES = 3000000 # Increase for fuzzing +MAX_SAMPLES = 1000000 # Increase for fuzzing +N_CASES = 1 A_MUL = 10000 MIN_A = int(N_COINS**N_COINS * A_MUL / 10) @@ -47,27 +47,26 @@ def inv_target_decimal_n2(A, gamma, x, D): MIN_GAMMA = 10**10 MAX_GAMMA = 5 * 10**16 -MIN_XD = 10**16 - 1 -MAX_XD = 10**20 + 1 +MIN_XD = 10**17 +MAX_XD = 10**19 pytest.progress = 0 -pytest.positive_dy = 0 +pytest.actually_tested = 0 pytest.t_start = time.time() -pytest.gas_original = 0 -pytest.gas_new = 0 -failed_cases = [] - +@pytest.mark.parametrize( + "_tmp", range(N_CASES) + ) # Create N_CASES independent test instances. @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( min_value=10**18, max_value=10**14 * 10**18 ), # 1 USD to 100T USD xD=st.integers( - min_value=int(1.001e16), max_value=int(0.999e20) + min_value=MIN_XD, max_value=MAX_XD ), # <- ratio 1e18 * x/D, typically 1e18 * 1 yD=st.integers( - min_value=int(1.001e16), max_value=int(0.999e20) + min_value=MIN_XD, max_value=MAX_XD ), # <- ratio 1e18 * y/D, typically 1e18 * 1 gamma=st.integers(min_value=MIN_GAMMA, max_value=MAX_GAMMA), j=st.integers(min_value=0, max_value=1), @@ -99,6 +98,7 @@ def test_newton_D( mid_fee, out_fee, fee_gamma, + _tmp, ): _test_newton_D( math_optimized, @@ -114,6 +114,7 @@ def test_newton_D( mid_fee, out_fee, fee_gamma, + _tmp, ) @@ -131,6 +132,7 @@ def _test_newton_D( mid_fee, out_fee, fee_gamma, + _tmp, ): is_safe = all( @@ -139,11 +141,8 @@ def _test_newton_D( ) pytest.progress += 1 - if pytest.progress % 1000 == 0 and pytest.positive_dy != 0: - print( - f"{pytest.progress} | {pytest.positive_dy} cases processed in {time.time()-pytest.t_start:.1f} seconds." - f"Gas advantage per call: {pytest.gas_original//pytest.positive_dy} {pytest.gas_new//pytest.positive_dy}\n" - ) + if pytest.progress % 1000 == 0 and pytest.actually_tested != 0: + print(f"{pytest.progress} | {pytest.actually_tested} cases processed in {time.time()-pytest.t_start:.1f} seconds.") X = [D * xD // 10**18, D * yD // 10**18] result_get_y = 0 @@ -156,7 +155,7 @@ def _test_newton_D( if get_y_failed: newton_y_failed = False try: - math_optimized.internal._newton_y(A, gamma, X, D, j) + math_optimized.newton_y(A, gamma, X, D, j) except: newton_y_failed = True @@ -183,58 +182,35 @@ def _test_newton_D( if dy / X[j] <= 0.95: - pytest.positive_dy += 1 + pytest.actually_tested += 1 X[j] = y - try: - result_sim = math_unoptimized.newton_D(A, gamma, X) - except: - raise # this is a problem + case = ( + "{" + f"'ANN': {A}, 'D': {D}, 'xD': {xD}, 'yD': {yD}, 'GAMMA': {gamma}, 'j': {j}, 'btcScalePrice': {btcScalePrice}, 'ethScalePrice': {ethScalePrice}, 'mid_fee': {mid_fee}, 'out_fee': {out_fee}, 'fee_gamma': {fee_gamma}" + "},\n" + ) + result_sim = math_unoptimized.newton_D(A, gamma, X) try: - result_sim = math_unoptimized.newton_D(A, gamma, X) - pytest.gas_original += ( - math_unoptimized._computation.get_gas_used() - ) - try: - result_contract = math_optimized.newton_D(A, gamma, X, K0) - pytest.gas_new += ( - math_optimized._computation.get_gas_used() - ) - except BoaError as e: - # print(e) - case = ( - "{" - f"'ANN': {A}, 'D': {D}, 'xD': {xD}, 'yD': {yD}, 'GAMMA': {gamma}, 'j': {j}, 'btcScalePrice': {btcScalePrice}, 'ethScalePrice': {ethScalePrice}, 'mid_fee': {mid_fee}, 'out_fee': {out_fee}, 'fee_gamma': {fee_gamma}" - "},\n" - ) - print(case) - raise - - A_dec = Decimal(A) / 10000 / 4 - - def calculate_D_polynome(d): - d = Decimal(d) - return abs(inv_target_decimal_n2(A_dec, gamma, X, d)) - - # print(f"ANN={A}; GAMMA={gamma}; x={X}") - - # D0 = int(2 * (X[0]*X[1])**(Decimal(1)/2)) - # D0_new = int((10**18*4*X[0]*X[1]//K0)**(Decimal(1)/2)) - # print(math_unoptimized._computation.get_gas_used(), D0, calculate_D_polynome(D0)) - # print(math_optimized._computation.get_gas_used(), D0_new, calculate_D_polynome(D0_new)) - - try: - assert abs(result_sim - result_contract) <= max( - 10000, result_sim / 1e12 - ) - except AssertionError: - case = ( - "{" - f"'ANN': {A}, 'D': {D}, 'xD': {xD}, 'yD': {yD}, 'GAMMA': {gamma}, 'j': {j}, 'btcScalePrice': {btcScalePrice}, 'ethScalePrice': {ethScalePrice}, 'mid_fee': {mid_fee}, 'out_fee': {out_fee}, 'fee_gamma': {fee_gamma}" - "},\n" - ) - with open("newton_D_n=2_cases.txt", "a") as f: - f.write(case) - except: - raise + result_contract = math_optimized.newton_D(A, gamma, X, K0) + except Exception as e: + # with open("log/newton_D_fail.txt", "a") as f: + # f.write(case) + # with open("log/newton_D_fail_trace.txt", "a") as f: + # f.write(str(e)) + return + + A_dec = Decimal(A) / 10000 / 4 + + def calculate_D_polynome(d): + d = Decimal(d) + return abs(inv_target_decimal_n2(A_dec, gamma, X, d)) + + assert abs(result_sim - result_contract) <= max( + 10000, result_sim / 1e12 + ) + + # with open("log/newton_D_pass.txt", "a") as f: + # f.write(case) + From 4cc4b743803e5916f3e27bdd99553e0ce748173a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:49:35 +0530 Subject: [PATCH 66/72] fix test MAX_A lim and fix order of unpacking in amm impl constructor --- contracts/main/CurveTwocryptoFactory.vy | 24 ++++++++++----------- contracts/main/CurveTwocryptoOptimized.vy | 14 ++++++------ requirements.txt | 2 +- tests/fixtures/pool.py | 26 +++++++++++------------ tests/unitary/math/test_newton_D.py | 18 ++++++++++------ tests/unitary/math/test_newton_D_ref.py | 6 ++++-- tests/unitary/math/test_packing.py | 10 +++++---- tests/unitary/pool/test_a_gamma.py | 1 - tests/utils/simulation_int_many.py | 5 +---- 9 files changed, 55 insertions(+), 51 deletions(-) diff --git a/contracts/main/CurveTwocryptoFactory.vy b/contracts/main/CurveTwocryptoFactory.vy index e82eaeb6..93ed59c7 100644 --- a/contracts/main/CurveTwocryptoFactory.vy +++ b/contracts/main/CurveTwocryptoFactory.vy @@ -1,5 +1,5 @@ # pragma version 0.3.10 -# pragma optimize gas +# pragma optimize codesize """ @title CurveTwocryptoFactory @author Curve.Fi @@ -186,17 +186,17 @@ def deploy_pool( # pool is an ERC20 implementation _salt: bytes32 = block.prevhash pool: address = create_from_blueprint( - pool_implementation, - _name, - _symbol, - _coins, - _math_implementation, - _salt, - precisions, - packed_A_gamma, - packed_fee_params, - packed_rebalancing_params, - initial_price, + pool_implementation, # blueprint: address + _name, # String[64] + _symbol, # String[32] + _coins, # address[N_COINS] + _math_implementation, # address + _salt, # bytes32 + precisions, # uint256[N_COINS] + packed_A_gamma, # uint256 + packed_fee_params, # uint256 + packed_rebalancing_params, # uint256 + initial_price, # uint256 code_offset=3 ) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 99bd487c..b7a0f9dc 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -123,7 +123,6 @@ event ClaimAdminFee: N_COINS: constant(uint256) = 2 PRECISION: constant(uint256) = 10**18 # <------- The precision to convert to. -A_MULTIPLIER: constant(uint256) = 10000 PRECISIONS: immutable(uint256[N_COINS]) MATH: public(immutable(Math)) @@ -178,8 +177,9 @@ admin_lp_virtual_balance: uint256 MIN_RAMP_TIME: constant(uint256) = 86400 MIN_ADMIN_FEE_CLAIM_INTERVAL: constant(uint256) = 86400 +A_MULTIPLIER: constant(uint256) = 10000 MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 -MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 100000 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 MAX_A_CHANGE: constant(uint256) = 10 MIN_GAMMA: constant(uint256) = 10**10 MAX_GAMMA: constant(uint256) = 5 * 10**16 @@ -235,13 +235,13 @@ def __init__( PRECISIONS = precisions # <------------------------ Precisions of coins. # --------------- Validate A and gamma parameters here and not in factory. - A_gamma: uint256[2] = self._unpack_2(packed_A_gamma) + gamma_A: uint256[2] = self._unpack_2(packed_A_gamma) # gamma is at idx 0. - assert A_gamma[0] > MIN_A-1 - assert A_gamma[0] < MAX_A+1 + assert gamma_A[0] > MIN_GAMMA-1 + assert gamma_A[0] < MAX_GAMMA+1 - assert A_gamma[1] > MIN_GAMMA-1 - assert A_gamma[1] < MAX_GAMMA+1 + assert gamma_A[1] > MIN_A-1 + assert gamma_A[1] < MAX_A+1 self.initial_A_gamma = packed_A_gamma self.future_A_gamma = packed_A_gamma diff --git a/requirements.txt b/requirements.txt index 89b2bded..60e57175 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,5 @@ pdbpp hypothesis>=6.68.1 # vyper and dev framework: -git+https://github.com/vyperlang/titanoboa@b5e9fb96d1424ed5cc5a6af03391d885439c83e5 +git+https://github.com/vyperlang/titanoboa@6d1faa3ba5b6a5c7823a7313b74b73dd551aebe4 vyper>=0.3.10 diff --git a/tests/fixtures/pool.py b/tests/fixtures/pool.py index 26f7acb5..8f911764 100644 --- a/tests/fixtures/pool.py +++ b/tests/fixtures/pool.py @@ -75,19 +75,19 @@ def swap( with boa.env.prank(deployer): swap = twocrypto_factory.deploy_pool( - "Curve.fi USD<>WETH", - "USD<>WETH", - [coin.address for coin in coins], - 0, # <-------- 0th implementation index - params["A"], - params["gamma"], - params["mid_fee"], - params["out_fee"], - params["fee_gamma"], - params["allowed_extra_profit"], - params["adjustment_step"], - params["ma_time"], # <--- no admin_fee needed - params["initial_prices"][1], + "Curve.fi USD<>WETH", # _name: String[64] + "USD<>WETH", # _symbol: String[32] + [coin.address for coin in coins], # _coins: address[N_COINS] + 0, # implementation_id: uint256 + params["A"], # A: uint256 + params["gamma"], # gamma: uint256 + params["mid_fee"], # mid_fee: uint256 + params["out_fee"], # out_fee: uint256 + params["fee_gamma"], # fee_gamma: uint256 + params["allowed_extra_profit"], # allowed_extra_profit: uint256 + params["adjustment_step"], # adjustment_step: uint256 + params["ma_time"], # ma_exp_time: uint256 + params["initial_prices"][1], # initial_price: uint256 ) return amm_interface.at(swap) diff --git a/tests/unitary/math/test_newton_D.py b/tests/unitary/math/test_newton_D.py index 5128b8e8..d11b9bf5 100644 --- a/tests/unitary/math/test_newton_D.py +++ b/tests/unitary/math/test_newton_D.py @@ -2,16 +2,18 @@ import sys import time from decimal import Decimal + import pytest from boa.vyper.contract import BoaError from hypothesis import given, settings from hypothesis import strategies as st -import tests.utils.simulation_int_many as sim +import tests.utils.simulation_int_many as sim # Uncomment to be able to print when parallelized # sys.stdout = sys.stderr + def inv_target_decimal_n2(A, gamma, x, D): N = len(x) @@ -41,7 +43,7 @@ def inv_target_decimal_n2(A, gamma, x, D): A_MUL = 10000 MIN_A = int(N_COINS**N_COINS * A_MUL / 10) -MAX_A = int(N_COINS**N_COINS * A_MUL * 100000) +MAX_A = int(N_COINS**N_COINS * A_MUL * 1000) # gamma from 1e-8 up to 0.05 MIN_GAMMA = 10**10 @@ -54,9 +56,10 @@ def inv_target_decimal_n2(A, gamma, x, D): pytest.actually_tested = 0 pytest.t_start = time.time() + @pytest.mark.parametrize( - "_tmp", range(N_CASES) - ) # Create N_CASES independent test instances. + "_tmp", range(N_CASES) +) # Create N_CASES independent test instances. @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( @@ -142,7 +145,9 @@ def _test_newton_D( pytest.progress += 1 if pytest.progress % 1000 == 0 and pytest.actually_tested != 0: - print(f"{pytest.progress} | {pytest.actually_tested} cases processed in {time.time()-pytest.t_start:.1f} seconds.") + print( + f"{pytest.progress} | {pytest.actually_tested} cases processed in {time.time()-pytest.t_start:.1f} seconds." + ) X = [D * xD // 10**18, D * yD // 10**18] result_get_y = 0 @@ -199,7 +204,7 @@ def _test_newton_D( # f.write(case) # with open("log/newton_D_fail_trace.txt", "a") as f: # f.write(str(e)) - return + return A_dec = Decimal(A) / 10000 / 4 @@ -213,4 +218,3 @@ def calculate_D_polynome(d): # with open("log/newton_D_pass.txt", "a") as f: # f.write(case) - diff --git a/tests/unitary/math/test_newton_D_ref.py b/tests/unitary/math/test_newton_D_ref.py index 489d4860..aa726e4c 100644 --- a/tests/unitary/math/test_newton_D_ref.py +++ b/tests/unitary/math/test_newton_D_ref.py @@ -2,8 +2,8 @@ import sys from decimal import Decimal -from boa.vyper.contract import BoaError import pytest +from boa.vyper.contract import BoaError from hypothesis import given, settings from hypothesis import strategies as st @@ -52,9 +52,11 @@ def inv_target_decimal_n2(A, gamma, x, D): MAX_XD = 10**19 pytest.cases = 0 + + @pytest.mark.parametrize( "_tmp", range(N_CASES) - ) # Create N_CASES independent test instances. +) # Create N_CASES independent test instances. @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( diff --git a/tests/unitary/math/test_packing.py b/tests/unitary/math/test_packing.py index 64de9215..3cdd42ae 100644 --- a/tests/unitary/math/test_packing.py +++ b/tests/unitary/math/test_packing.py @@ -4,8 +4,9 @@ @given(val=strategy("uint256[3]", max_value=10**18)) @settings(max_examples=10000, deadline=None) -def test_pack_unpack_three_integers(swap, twocrypto_factory, val): - +def test_pack_unpack_three_integers( + swap, math_contract, twocrypto_factory, val +): for contract in [swap, twocrypto_factory]: packed = contract.internal._pack_3(val) unpacked = swap.internal._unpack_3(packed) # swap unpacks @@ -19,5 +20,6 @@ def test_pack_unpack_2_integers(swap, val): packed = swap.internal._pack_2(val[0], val[1]) unpacked = swap.internal._unpack_2(packed) # swap unpacks - for i in range(2): - assert unpacked[i] == val[i] + + assert unpacked[0] == val[0] + assert unpacked[1] == val[1] diff --git a/tests/unitary/pool/test_a_gamma.py b/tests/unitary/pool/test_a_gamma.py index 0e43517f..33167a5a 100644 --- a/tests/unitary/pool/test_a_gamma.py +++ b/tests/unitary/pool/test_a_gamma.py @@ -2,7 +2,6 @@ def test_A_gamma(swap, params): - A = swap.A() gamma = swap.gamma() diff --git a/tests/utils/simulation_int_many.py b/tests/utils/simulation_int_many.py index ee6ec084..d00d8149 100644 --- a/tests/utils/simulation_int_many.py +++ b/tests/utils/simulation_int_many.py @@ -172,10 +172,7 @@ def y(self, x, i, j): xp = self.xp() xp[i] = x * self.p[i] // 10**18 yp = solve_x(self.A, self.gamma, xp, self.D(), j) - try: - return yp * 10**18 // self.p[j] - except: - breakpoint() + return yp * 10**18 // self.p[j] def get_data(fname): From ab2080b53230f714c29e260ca409b6f6a8204d4a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Sat, 16 Dec 2023 10:25:01 +0530 Subject: [PATCH 67/72] remove admin fee auto claim in add_liquidity --- contracts/main/CurveTwocryptoOptimized.vy | 2 -- tests/unitary/pool/test_admin_fee.py | 10 ++++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index b7a0f9dc..0d95433d 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -471,8 +471,6 @@ def add_liquidity( @return uint256 Amount of LP tokens received by the `receiver """ - self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. - A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) diff --git a/tests/unitary/pool/test_admin_fee.py b/tests/unitary/pool/test_admin_fee.py index b6bef490..a314907d 100644 --- a/tests/unitary/pool/test_admin_fee.py +++ b/tests/unitary/pool/test_admin_fee.py @@ -33,15 +33,13 @@ def test_admin_fee_after_deposit( swap.exchange(1, 0, to_swap, 0) balances = [swap.balances(i) for i in range(2)] - print("Balance of the pool: " + str(balances[0]) + ", " + str(balances[1])) - print("Ratio:", ratio) split_quantities = [int(balances[0] * ratio), int(balances[1] * ratio)] with boa.env.prank(user): swap.add_liquidity(split_quantities, 0) - print("FEES 0: " + str(coins[0].balanceOf(fee_receiver))) - print("FEES 1: " + str(coins[1].balanceOf(fee_receiver))) - - print() + assert ( + coins[0].balanceOf(fee_receiver) + coins[1].balanceOf(fee_receiver) + == 0 + ) return swap From 7092010f29ddffeda863a4297cc560910facfebd Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Sat, 16 Dec 2023 10:44:29 +0530 Subject: [PATCH 68/72] pack precisions and name packed gamma and A var better --- contracts/main/CurveTwocryptoFactory.vy | 21 ++++++++++++++------- contracts/main/CurveTwocryptoOptimized.vy | 12 ++++++------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/contracts/main/CurveTwocryptoFactory.vy b/contracts/main/CurveTwocryptoFactory.vy index 93ed59c7..e125673c 100644 --- a/contracts/main/CurveTwocryptoFactory.vy +++ b/contracts/main/CurveTwocryptoFactory.vy @@ -102,7 +102,7 @@ def __init__(_fee_receiver: address, _admin: address): @internal -@view +@pure def _pack_3(x: uint256[3]) -> uint256: """ @notice Packs 3 integers with values <= 10**18 into a uint256 @@ -112,6 +112,11 @@ def _pack_3(x: uint256[3]) -> uint256: return (x[0] << 128) | (x[1] << 64) | x[2] +@pure +@internal +def _pack_2(p1: uint256, p2: uint256) -> uint256: + return p1 | (p2 << 128) + # <--- Pool Deployers ---> @@ -169,6 +174,9 @@ def deploy_pool( decimals[i] = d precisions[i] = 10 ** (18 - d) + # pack precision + packed_precisions: uint256 = self._pack_2(precisions[0], precisions[1]) + # pack fees packed_fee_params: uint256 = self._pack_3( [mid_fee, out_fee, fee_gamma] @@ -179,9 +187,8 @@ def deploy_pool( [allowed_extra_profit, adjustment_step, ma_exp_time] ) - # pack A_gamma - packed_A_gamma: uint256 = A << 128 - packed_A_gamma = packed_A_gamma | gamma + # pack gamma and A + packed_gamma_A: uint256 = self._pack_2(gamma, A) # pool is an ERC20 implementation _salt: bytes32 = block.prevhash @@ -192,8 +199,8 @@ def deploy_pool( _coins, # address[N_COINS] _math_implementation, # address _salt, # bytes32 - precisions, # uint256[N_COINS] - packed_A_gamma, # uint256 + packed_precisions, # uint256 + packed_gamma_A, # uint256 packed_fee_params, # uint256 packed_rebalancing_params, # uint256 initial_price, # uint256 @@ -218,7 +225,7 @@ def deploy_pool( _math_implementation, _salt, precisions, - packed_A_gamma, + packed_gamma_A, packed_fee_params, packed_rebalancing_params, initial_price, diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 0d95433d..0c58f3ec 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -218,8 +218,8 @@ def __init__( _coins: address[N_COINS], _math: address, _salt: bytes32, - precisions: uint256[N_COINS], - packed_A_gamma: uint256, + packed_precisions: uint256, + packed_gamma_A: uint256, packed_fee_params: uint256, packed_rebalancing_params: uint256, initial_price: uint256, @@ -232,10 +232,10 @@ def __init__( symbol = _symbol coins = _coins - PRECISIONS = precisions # <------------------------ Precisions of coins. + PRECISIONS = self._unpack_2(packed_precisions) # <-- Precisions of coins. # --------------- Validate A and gamma parameters here and not in factory. - gamma_A: uint256[2] = self._unpack_2(packed_A_gamma) # gamma is at idx 0. + gamma_A: uint256[2] = self._unpack_2(packed_gamma_A) # gamma is at idx 0. assert gamma_A[0] > MIN_GAMMA-1 assert gamma_A[0] < MAX_GAMMA+1 @@ -243,8 +243,8 @@ def __init__( assert gamma_A[1] > MIN_A-1 assert gamma_A[1] < MAX_A+1 - self.initial_A_gamma = packed_A_gamma - self.future_A_gamma = packed_A_gamma + self.initial_A_gamma = packed_gamma_A + self.future_A_gamma = packed_gamma_A # ------------------------------------------------------------------------ self.packed_rebalancing_params = packed_rebalancing_params # <-- Contains From 036a8e877d9d374ec14b7ae6e99ca745a3216aed Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:20:41 +0530 Subject: [PATCH 69/72] fix math tests --- requirements.txt | 2 +- tests/unitary/math/test_cbrt.py | 47 +++++++ tests/unitary/math/test_exp.py | 31 +++++ tests/unitary/math/test_get_p.py | 163 ++++++++++++++++++++++++ tests/unitary/math/test_log2.py | 28 ++++ tests/unitary/math/test_newton_D.py | 3 +- tests/unitary/math/test_newton_D_ref.py | 4 +- 7 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 tests/unitary/math/test_cbrt.py create mode 100644 tests/unitary/math/test_exp.py create mode 100644 tests/unitary/math/test_get_p.py create mode 100644 tests/unitary/math/test_log2.py diff --git a/requirements.txt b/requirements.txt index 60e57175..c950036a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,5 @@ pdbpp hypothesis>=6.68.1 # vyper and dev framework: -git+https://github.com/vyperlang/titanoboa@6d1faa3ba5b6a5c7823a7313b74b73dd551aebe4 +git+https://github.com/vyperlang/titanoboa@878da481a5adc3963b07b22355c80a85a9cfecb7 vyper>=0.3.10 diff --git a/tests/unitary/math/test_cbrt.py b/tests/unitary/math/test_cbrt.py new file mode 100644 index 00000000..a287945d --- /dev/null +++ b/tests/unitary/math/test_cbrt.py @@ -0,0 +1,47 @@ +import pytest +from boa.test import strategy +from hypothesis import example, given, settings +from vyper.utils import SizeLimits + +SETTINGS = {"max_examples": 10000, "deadline": None} +MAX_VAL = SizeLimits.MAX_UINT256 +MAX_CBRT_PRECISE_VAL = MAX_VAL // 10**36 + + +def test_cbrt_expected_output(cbrt_1e18_base, math_optimized): + vals = [9 * 10**18, 8 * 10**18, 10**18, 1] + correct_cbrts = [2080083823051904114, 2 * 10**18, 10**18, 10**12] + for ix, val in enumerate(vals): + assert math_optimized.internal._cbrt(val) == correct_cbrts[ix] + assert cbrt_1e18_base(val) == correct_cbrts[ix] + + +@given( + val=strategy("uint256", min_value=0, max_value=MAX_CBRT_PRECISE_VAL - 1) +) +@settings(**SETTINGS) +@example(0) +@example(1) +def test_cbrt_exact(math_optimized, cbrt_1e18_base, val): + + cbrt_python = cbrt_1e18_base(val) + cbrt_vyper = math_optimized.internal._cbrt(val) + + try: + assert cbrt_python == cbrt_vyper + except AssertionError: + assert abs(cbrt_python - cbrt_vyper) == 1 + pytest.warn(f"cbrt_python != cbrt_vyper for val = {val}") + + +@given( + val=strategy("uint256", min_value=MAX_CBRT_PRECISE_VAL, max_value=MAX_VAL) +) +@settings(**SETTINGS) +@example(MAX_VAL) +@example(MAX_CBRT_PRECISE_VAL) +def test_cbrt_precision_loss_gte_limit(cbrt_1e18_base, math_optimized, val): + + cbrt_vyper = math_optimized.internal._cbrt(val) + cbrt_python = cbrt_1e18_base(val) + assert cbrt_vyper == pytest.approx(cbrt_python) diff --git a/tests/unitary/math/test_exp.py b/tests/unitary/math/test_exp.py new file mode 100644 index 00000000..d5af9589 --- /dev/null +++ b/tests/unitary/math/test_exp.py @@ -0,0 +1,31 @@ +import math + +import boa +import pytest +from boa.test import strategy +from hypothesis import given, settings +from vyper.utils import SizeLimits + + +@given( + strategy( + "int256", + min_value=SizeLimits.MIN_INT256, + max_value=SizeLimits.MAX_INT256, + ) +) +@settings(max_examples=10000, deadline=None) +def test_exp(math_optimized, x): + + if x >= 135305999368893231589: + with boa.reverts("Math: wad_exp overflow"): + math_optimized.wad_exp(x) + + elif x <= -42139678854452767551: + assert math_optimized.wad_exp(x) == 0 + + else: + + exp_ideal = int(math.exp(x / 10**18) * 10**18) + exp_implementation = math_optimized.wad_exp(x) + assert exp_ideal == pytest.approx(exp_implementation) diff --git a/tests/unitary/math/test_get_p.py b/tests/unitary/math/test_get_p.py new file mode 100644 index 00000000..86fe2211 --- /dev/null +++ b/tests/unitary/math/test_get_p.py @@ -0,0 +1,163 @@ +from math import log + +import boa +import pytest +from boa.test import strategy +from hypothesis import given, settings + +from tests.fixtures.pool import INITIAL_PRICES +from tests.utils.tokens import mint_for_testing + +SETTINGS = {"max_examples": 100, "deadline": None} + + +# flake8: noqa: E501 +@pytest.fixture(scope="module") +def dydx_safemath(): + + get_price_impl = """ +N_COINS: constant(uint256) = 2 +A_MULTIPLIER: constant(uint256) = 10000 + +@external +@view +def get_p( + _xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[N_COINS] +) -> uint256: + + assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe D values + + K0: uint256 = 4 * _xp[0] * _xp[1] / _D * 10**36 / _D + GK0: uint256 = ( + 2 * K0 * K0 / 10**36 * K0 / 10**36 + + (_A_gamma[1] + 10**18)**2 + - K0**2 / 10**36 * (2 * _A_gamma[1] + 3 * 10**18) / 10**18 + ) + NNAG2: uint256 = _A_gamma[0] * _A_gamma[1]**2 / A_MULTIPLIER + denominator: uint256 = GK0 + NNAG2 * _xp[0] / _D * K0 / 10**36 + return _xp[0] * ( GK0 + NNAG2 * _xp[1] / _D * K0 / 10**36 ) / _xp[1] * 10**18 / denominator +""" + return boa.loads(get_price_impl) + + +def _get_dydx_vyper(swap, price_calc): + + xp = swap.internal.xp( + swap._storage.balances.get(), + swap.price_scale(), + ) + + return price_calc.get_p(xp, swap.D(), swap.internal._A_gamma()) + + +def _get_prices_vyper(swap, price_calc): + + price_token_1_wrt_0 = _get_dydx_vyper(swap, price_calc) + return price_token_1_wrt_0 * swap.price_scale() // 10**18 + + +def _get_prices_numeric_nofee(swap, views, sell_usd): + + if sell_usd: + + dx = 10**16 # 0.01 USD + dy = (views.internal._get_dy_nofee(0, 1, dx, swap)[0],) + price = dx * 10**18 // dy[0] + + else: + + dx = int(0.01 * 10**36 // INITIAL_PRICES[1]) + dolla_out = views.internal._get_dy_nofee(1, 0, dx, swap)[0] + price = dolla_out * 10**18 // dx + + return price + + +# ----- Tests ----- + + +@given( + dollar_amount=strategy( + "uint256", min_value=5 * 10**4, max_value=5 * 10**8 + ), +) +@settings(**SETTINGS) +@pytest.mark.parametrize("i", [0, 1]) +@pytest.mark.parametrize("j", [0, 1]) +def test_dxdy_similar( + yuge_swap, + dydx_safemath, + views_contract, + user, + dollar_amount, + coins, + i, + j, +): + + if i == j: + return + + dx = dollar_amount * 10**36 // INITIAL_PRICES[i] + mint_for_testing(coins[i], user, dx) + + with boa.env.prank(user): + yuge_swap.exchange(i, j, dx, 0) + + dxdy_vyper = _get_prices_vyper(yuge_swap, dydx_safemath) + dxdy_numeric_nofee = _get_prices_numeric_nofee( + yuge_swap, views_contract, sell_usd=(i == 0) + ) + + assert abs(log(dxdy_vyper / dxdy_numeric_nofee)) < 1e-5 + + dxdy_swap = yuge_swap.last_prices() # <-- we check unsafe impl here. + assert abs(log(dxdy_vyper / dxdy_swap)) < 1e-5 + + +@given( + dollar_amount=strategy( + "uint256", min_value=10**4, max_value=4 * 10**5 + ), +) +@settings(**SETTINGS) +def test_dxdy_pump(yuge_swap, dydx_safemath, user, dollar_amount, coins): + + dxdy_math_0 = _get_prices_vyper(yuge_swap, dydx_safemath) + dxdy_swap_0 = yuge_swap.last_prices() + + dx = dollar_amount * 10**18 + mint_for_testing(coins[0], user, dx) + + with boa.env.prank(user): + yuge_swap.exchange(0, 1, dx, 0) + + dxdy_math_1 = _get_prices_vyper(yuge_swap, dydx_safemath) + dxdy_swap_1 = yuge_swap.last_prices() + + assert dxdy_math_1 > dxdy_math_0 + assert dxdy_swap_1 > dxdy_swap_0 + + +@given( + dollar_amount=strategy( + "uint256", min_value=10**4, max_value=4 * 10**5 + ), +) +@settings(**SETTINGS) +def test_dxdy_dump(yuge_swap, dydx_safemath, user, dollar_amount, coins): + + dxdy_math_0 = _get_prices_vyper(yuge_swap, dydx_safemath) + dxdy_swap_0 = yuge_swap.last_prices() + + dx = dollar_amount * 10**36 // INITIAL_PRICES[1] + mint_for_testing(coins[1], user, dx) + + with boa.env.prank(user): + yuge_swap.exchange(1, 0, dx, 0) + + dxdy_math_1 = _get_prices_vyper(yuge_swap, dydx_safemath) + dxdy_swap_1 = yuge_swap.last_prices() + + assert dxdy_math_1 < dxdy_math_0 + assert dxdy_swap_1 < dxdy_swap_0 diff --git a/tests/unitary/math/test_log2.py b/tests/unitary/math/test_log2.py new file mode 100644 index 00000000..4c94d9f6 --- /dev/null +++ b/tests/unitary/math/test_log2.py @@ -0,0 +1,28 @@ +import math + +from boa.test import strategy +from hypothesis import given, settings +from vyper.utils import SizeLimits + + +@given( + strategy( + "uint256", + min_value=0, + max_value=SizeLimits.MAX_UINT256, + ) +) +@settings(max_examples=10000, deadline=None) +def test_log2(math_optimized, x): + + if x == 0: + assert math_optimized.internal._snekmate_log_2(x, False) == 0 + return + + log2_ideal = int(math.log2(x)) + log2_implementation = math_optimized.internal._snekmate_log_2(x, False) + + try: + assert log2_ideal == log2_implementation + except: # noqa: E722; there will be off-by-one cases + assert abs(log2_ideal - log2_implementation) == 1 diff --git a/tests/unitary/math/test_newton_D.py b/tests/unitary/math/test_newton_D.py index d11b9bf5..d1cfe5b7 100644 --- a/tests/unitary/math/test_newton_D.py +++ b/tests/unitary/math/test_newton_D.py @@ -38,7 +38,8 @@ def inv_target_decimal_n2(A, gamma, x, D): N_COINS = 2 -MAX_SAMPLES = 1000000 # Increase for fuzzing +# MAX_SAMPLES = 1000000 # Increase for fuzzing +MAX_SAMPLES = 300 # Increase for fuzzing N_CASES = 1 A_MUL = 10000 diff --git a/tests/unitary/math/test_newton_D_ref.py b/tests/unitary/math/test_newton_D_ref.py index aa726e4c..6852c39d 100644 --- a/tests/unitary/math/test_newton_D_ref.py +++ b/tests/unitary/math/test_newton_D_ref.py @@ -36,8 +36,8 @@ def inv_target_decimal_n2(A, gamma, x, D): N_COINS = 2 -MAX_SAMPLES = 300000 # Increase for fuzzing -# MAX_SAMPLES = 300 # Increase for fuzzing +# MAX_SAMPLES = 300000 # Increase for fuzzing +MAX_SAMPLES = 300 # Increase for fuzzing N_CASES = 1 A_MUL = 10000 From 6246ad9cb71078c80b9daf8aaa2f2b450e652169 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Sat, 23 Dec 2023 11:54:01 +0530 Subject: [PATCH 70/72] stateful tests and other tests --- contracts/main/CurveCryptoViews2Optimized.vy | 4 +- contracts/main/CurveTwocryptoOptimized.vy | 10 + tests/fixtures/accounts.py | 4 +- tests/fixtures/factory.py | 2 +- tests/fixtures/pool.py | 42 ++- tests/fixtures/tokens.py | 2 +- tests/unitary/math/test_get_p.py | 89 ++---- tests/unitary/math/test_get_y.py | 3 +- tests/unitary/math/test_packing.py | 6 +- .../unitary/pool/admin/test_commit_params.py | 133 +++++++++ tests/unitary/pool/admin/test_ramp_A_gamma.py | 49 +++ .../pool/admin/test_revert_commit_params.py | 81 +++++ tests/unitary/pool/admin/test_revert_ramp.py | 41 +++ tests/unitary/pool/stateful/stateful_base.py | 160 +++++++--- .../pool/stateful/test_add_liquidity.py | 38 --- .../pool/stateful/test_multiprecision.py | 54 ++++ tests/unitary/pool/stateful/test_ramp.py | 107 +++++++ .../pool/stateful/test_ramp_nocheck.py | 81 +++++ tests/unitary/pool/stateful/test_simulate.py | 133 +++++++++ tests/unitary/pool/stateful/test_stateful.py | 182 ++++++++++++ tests/unitary/pool/test_exchange_received.py | 33 --- tests/unitary/pool/test_oracles.py | 222 ++++++++++++++ tests/unitary/pool/token/conftest.py | 58 ++++ tests/unitary/pool/token/test_approve.py | 81 +++++ tests/unitary/pool/token/test_permit.py | 92 ++++++ tests/unitary/pool/token/test_transfer.py | 91 ++++++ tests/unitary/pool/token/test_transferFrom.py | 212 +++++++++++++ tests/utils/simulation_int_many.py | 280 +++--------------- 28 files changed, 1843 insertions(+), 447 deletions(-) create mode 100644 tests/unitary/pool/admin/test_commit_params.py create mode 100644 tests/unitary/pool/admin/test_ramp_A_gamma.py create mode 100644 tests/unitary/pool/admin/test_revert_commit_params.py create mode 100644 tests/unitary/pool/admin/test_revert_ramp.py delete mode 100644 tests/unitary/pool/stateful/test_add_liquidity.py create mode 100644 tests/unitary/pool/stateful/test_multiprecision.py create mode 100644 tests/unitary/pool/stateful/test_ramp.py create mode 100644 tests/unitary/pool/stateful/test_ramp_nocheck.py create mode 100644 tests/unitary/pool/stateful/test_simulate.py create mode 100644 tests/unitary/pool/stateful/test_stateful.py delete mode 100644 tests/unitary/pool/test_exchange_received.py create mode 100644 tests/unitary/pool/test_oracles.py create mode 100644 tests/unitary/pool/token/conftest.py create mode 100644 tests/unitary/pool/token/test_approve.py create mode 100644 tests/unitary/pool/token/test_permit.py create mode 100644 tests/unitary/pool/token/test_transfer.py create mode 100644 tests/unitary/pool/token/test_transferFrom.py diff --git a/contracts/main/CurveCryptoViews2Optimized.vy b/contracts/main/CurveCryptoViews2Optimized.vy index b3ad8238..90c62640 100644 --- a/contracts/main/CurveCryptoViews2Optimized.vy +++ b/contracts/main/CurveCryptoViews2Optimized.vy @@ -359,7 +359,7 @@ def _calc_withdraw_one_coin( def _fee(xp: uint256[N_COINS], swap: address) -> uint256: packed_fee_params: uint256 = Curve(swap).packed_fee_params() - fee_params: uint256[3] = self._unpack(packed_fee_params) + fee_params: uint256[3] = self._unpack_3(packed_fee_params) f: uint256 = xp[0] + xp[1] f = fee_params[2] * 10**18 / ( fee_params[2] + 10**18 - @@ -400,7 +400,7 @@ def _prep_calc(swap: address) -> ( @internal @view -def _unpack(_packed: uint256) -> uint256[3]: +def _unpack_3(_packed: uint256) -> uint256[3]: """ @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) @param val The uint256 to unpack diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 0c58f3ec..38195ed3 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -1563,6 +1563,16 @@ def fee_receiver() -> address: return factory.fee_receiver() +@external +@view +def admin() -> address: + """ + @notice Returns the address of the pool's admin. + @return address Admin. + """ + return factory.admin() + + @external @view def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256: diff --git a/tests/fixtures/accounts.py b/tests/fixtures/accounts.py index 3099db89..70b91065 100644 --- a/tests/fixtures/accounts.py +++ b/tests/fixtures/accounts.py @@ -21,8 +21,8 @@ def hacker(): @pytest.fixture(scope="module") -def factory_admin(twocrypto_factory): - return twocrypto_factory.admin() +def factory_admin(factory): + return factory.admin() @pytest.fixture(scope="module") diff --git a/tests/fixtures/factory.py b/tests/fixtures/factory.py index cf3a8986..18fe3dc5 100644 --- a/tests/fixtures/factory.py +++ b/tests/fixtures/factory.py @@ -37,7 +37,7 @@ def views_contract(deployer): @pytest.fixture(scope="module") -def twocrypto_factory( +def factory( deployer, fee_receiver, owner, diff --git a/tests/fixtures/pool.py b/tests/fixtures/pool.py index 8f911764..de4c2f66 100644 --- a/tests/fixtures/pool.py +++ b/tests/fixtures/pool.py @@ -66,7 +66,7 @@ def params(): @pytest.fixture(scope="module") def swap( - twocrypto_factory, + factory, amm_interface, coins, params, @@ -74,7 +74,7 @@ def swap( ): with boa.env.prank(deployer): - swap = twocrypto_factory.deploy_pool( + swap = factory.deploy_pool( "Curve.fi USD<>WETH", # _name: String[64] "USD<>WETH", # _symbol: String[32] [coin.address for coin in coins], # _coins: address[N_COINS] @@ -95,11 +95,10 @@ def swap( @pytest.fixture(scope="module") def swap_multiprecision( - twocrypto_factory, + factory, amm_interface, stgusdc, deployer, - weth, ): # STG/USDC pool params (on deployment) @@ -112,26 +111,25 @@ def swap_multiprecision( "fee_gamma": 230000000000000, "adjustment_step": 146000000000000, "ma_time": 866, - "initial_prices": 500000000000000000, + "initial_prices": 1777655918836068423, } - with boa.env.prank(deployer): - swap = twocrypto_factory.deploy_pool( - "Curve.fi STG/USDC", - "STGUSDC", - [coin.address for coin in stgusdc], - weth, - 0, - _params["A"], - _params["gamma"], - _params["mid_fee"], - _params["out_fee"], - _params["fee_gamma"], - _params["allowed_extra_profit"], - _params["adjustment_step"], - _params["ma_time"], - _params["initial_prices"], - ) + swap = factory.deploy_pool( + "Curve.fi STG/USDC", + "STGUSDC", + [coin.address for coin in stgusdc], # _coins: address[N_COINS] + 0, # implementation_id: uint256 + _params["A"], # A: uint256 + _params["gamma"], # gamma: uint256 + _params["mid_fee"], # mid_fee: uint256 + _params["out_fee"], # out_fee: uint256 + _params["fee_gamma"], # fee_gamma: uint256 + _params["allowed_extra_profit"], # allowed_extra_profit: uint256 + _params["adjustment_step"], # adjustment_step: uint256 + _params["ma_time"], # ma_exp_time: uint256 + _params["initial_prices"], # initial_price: uint256 + sender=deployer, + ) return amm_interface.at(swap) diff --git a/tests/fixtures/tokens.py b/tests/fixtures/tokens.py index bc553440..d16e384d 100644 --- a/tests/fixtures/tokens.py +++ b/tests/fixtures/tokens.py @@ -50,7 +50,7 @@ def coins(usd, weth): @pytest.fixture(scope="module") -def stgusdc(usdt, weth): +def stgusdc(stg, usdc): yield [stg, usdc] diff --git a/tests/unitary/math/test_get_p.py b/tests/unitary/math/test_get_p.py index 86fe2211..3ebf6bfb 100644 --- a/tests/unitary/math/test_get_p.py +++ b/tests/unitary/math/test_get_p.py @@ -8,7 +8,7 @@ from tests.fixtures.pool import INITIAL_PRICES from tests.utils.tokens import mint_for_testing -SETTINGS = {"max_examples": 100, "deadline": None} +SETTINGS = {"max_examples": 1000, "deadline": None} # flake8: noqa: E501 @@ -35,7 +35,8 @@ def get_p( ) NNAG2: uint256 = _A_gamma[0] * _A_gamma[1]**2 / A_MULTIPLIER denominator: uint256 = GK0 + NNAG2 * _xp[0] / _D * K0 / 10**36 - return _xp[0] * ( GK0 + NNAG2 * _xp[1] / _D * K0 / 10**36 ) / _xp[1] * 10**18 / denominator + numerator: uint256 = _xp[0] * ( GK0 + NNAG2 * _xp[1] / _D * K0 / 10**36 ) / _xp[1] + return numerator * 10**18 / denominator """ return boa.loads(get_price_impl) @@ -56,15 +57,15 @@ def _get_prices_vyper(swap, price_calc): return price_token_1_wrt_0 * swap.price_scale() // 10**18 -def _get_prices_numeric_nofee(swap, views, sell_usd): +def _get_prices_numeric_nofee(swap, views, i): - if sell_usd: + if i == 0: # we are selling j dx = 10**16 # 0.01 USD - dy = (views.internal._get_dy_nofee(0, 1, dx, swap)[0],) + dy = views.internal._get_dy_nofee(0, 1, dx, swap)[0] price = dx * 10**18 // dy[0] - else: + else: # we are buying j so numba should go up dx = int(0.01 * 10**36 // INITIAL_PRICES[1]) dolla_out = views.internal._get_dy_nofee(1, 0, dx, swap)[0] @@ -78,12 +79,11 @@ def _get_prices_numeric_nofee(swap, views, sell_usd): @given( dollar_amount=strategy( - "uint256", min_value=5 * 10**4, max_value=5 * 10**8 + "decimal", min_value=10**-5, max_value=5 * 10**8 ), ) @settings(**SETTINGS) @pytest.mark.parametrize("i", [0, 1]) -@pytest.mark.parametrize("j", [0, 1]) def test_dxdy_similar( yuge_swap, dydx_safemath, @@ -92,72 +92,27 @@ def test_dxdy_similar( dollar_amount, coins, i, - j, ): - if i == j: - return + previous_p = yuge_swap.price_scale() + j = 1 - i - dx = dollar_amount * 10**36 // INITIAL_PRICES[i] + dx = int(dollar_amount * 10**36 // INITIAL_PRICES[i]) mint_for_testing(coins[i], user, dx) - - with boa.env.prank(user): - yuge_swap.exchange(i, j, dx, 0) + out = yuge_swap.exchange(i, j, dx, 0, sender=user) dxdy_vyper = _get_prices_vyper(yuge_swap, dydx_safemath) + dxdy_swap = yuge_swap.last_prices() # <-- we check unsafe impl here. dxdy_numeric_nofee = _get_prices_numeric_nofee( - yuge_swap, views_contract, sell_usd=(i == 0) + yuge_swap, views_contract, i ) - assert abs(log(dxdy_vyper / dxdy_numeric_nofee)) < 1e-5 - - dxdy_swap = yuge_swap.last_prices() # <-- we check unsafe impl here. - assert abs(log(dxdy_vyper / dxdy_swap)) < 1e-5 - - -@given( - dollar_amount=strategy( - "uint256", min_value=10**4, max_value=4 * 10**5 - ), -) -@settings(**SETTINGS) -def test_dxdy_pump(yuge_swap, dydx_safemath, user, dollar_amount, coins): - - dxdy_math_0 = _get_prices_vyper(yuge_swap, dydx_safemath) - dxdy_swap_0 = yuge_swap.last_prices() - - dx = dollar_amount * 10**18 - mint_for_testing(coins[0], user, dx) - - with boa.env.prank(user): - yuge_swap.exchange(0, 1, dx, 0) + if i == 0: # j is being pupmed + assert dxdy_swap > previous_p + assert dxdy_numeric_nofee > previous_p + else: # j is being dupmed + assert dxdy_swap < previous_p + assert dxdy_numeric_nofee < previous_p - dxdy_math_1 = _get_prices_vyper(yuge_swap, dydx_safemath) - dxdy_swap_1 = yuge_swap.last_prices() - - assert dxdy_math_1 > dxdy_math_0 - assert dxdy_swap_1 > dxdy_swap_0 - - -@given( - dollar_amount=strategy( - "uint256", min_value=10**4, max_value=4 * 10**5 - ), -) -@settings(**SETTINGS) -def test_dxdy_dump(yuge_swap, dydx_safemath, user, dollar_amount, coins): - - dxdy_math_0 = _get_prices_vyper(yuge_swap, dydx_safemath) - dxdy_swap_0 = yuge_swap.last_prices() - - dx = dollar_amount * 10**36 // INITIAL_PRICES[1] - mint_for_testing(coins[1], user, dx) - - with boa.env.prank(user): - yuge_swap.exchange(1, 0, dx, 0) - - dxdy_math_1 = _get_prices_vyper(yuge_swap, dydx_safemath) - dxdy_swap_1 = yuge_swap.last_prices() - - assert dxdy_math_1 < dxdy_math_0 - assert dxdy_swap_1 < dxdy_swap_0 + assert dxdy_vyper == dxdy_swap + assert abs(log(dxdy_vyper / dxdy_numeric_nofee)) < 1e-5 diff --git a/tests/unitary/math/test_get_y.py b/tests/unitary/math/test_get_y.py index f923df84..a082062b 100644 --- a/tests/unitary/math/test_get_y.py +++ b/tests/unitary/math/test_get_y.py @@ -8,7 +8,8 @@ from hypothesis import strategies as st N_COINS = 2 -MAX_SAMPLES = 1000000 # Increase for fuzzing +# MAX_SAMPLES = 1000000 # Increase for fuzzing +MAX_SAMPLES = 300 N_CASES = 32 A_MUL = 10000 diff --git a/tests/unitary/math/test_packing.py b/tests/unitary/math/test_packing.py index 3cdd42ae..de444c48 100644 --- a/tests/unitary/math/test_packing.py +++ b/tests/unitary/math/test_packing.py @@ -4,10 +4,8 @@ @given(val=strategy("uint256[3]", max_value=10**18)) @settings(max_examples=10000, deadline=None) -def test_pack_unpack_three_integers( - swap, math_contract, twocrypto_factory, val -): - for contract in [swap, twocrypto_factory]: +def test_pack_unpack_three_integers(swap, factory, val): + for contract in [swap, factory]: packed = contract.internal._pack_3(val) unpacked = swap.internal._unpack_3(packed) # swap unpacks for i in range(3): diff --git a/tests/unitary/pool/admin/test_commit_params.py b/tests/unitary/pool/admin/test_commit_params.py new file mode 100644 index 00000000..215ec7e6 --- /dev/null +++ b/tests/unitary/pool/admin/test_commit_params.py @@ -0,0 +1,133 @@ +import copy + +import boa + + +def _apply_new_params(swap, params): + swap.apply_new_parameters( + params["mid_fee"], + params["out_fee"], + params["fee_gamma"], + params["allowed_extra_profit"], + params["adjustment_step"], + params["ma_time"], + params["xcp_ma_time"], + ) + + +def test_commit_accept_mid_fee(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["mid_fee"] = p["mid_fee"] + 1 + with boa.env.prank(factory_admin): + _apply_new_params(swap, p) + + mid_fee = swap.internal._unpack_3(swap._storage.packed_fee_params.get())[0] + assert mid_fee == p["mid_fee"] + + +def test_commit_accept_out_fee(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["out_fee"] = p["out_fee"] + 1 + with boa.env.prank(factory_admin): + _apply_new_params(swap, p) + + out_fee = swap.internal._unpack_3(swap._storage.packed_fee_params.get())[1] + assert out_fee == p["out_fee"] + + +def test_commit_accept_fee_gamma(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["fee_gamma"] = 10**17 + with boa.env.prank(factory_admin): + _apply_new_params(swap, p) + + fee_gamma = swap.internal._unpack_3(swap._storage.packed_fee_params.get())[ + 2 + ] + assert fee_gamma == p["fee_gamma"] + + +def test_commit_accept_fee_params(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["mid_fee"] += 1 + p["out_fee"] += 1 + p["fee_gamma"] = 10**17 + + with boa.env.prank(swap.admin()): + _apply_new_params(swap, p) + + fee_params = swap.internal._unpack_3(swap._storage.packed_fee_params.get()) + assert fee_params[0] == p["mid_fee"] + assert fee_params[1] == p["out_fee"] + assert fee_params[2] == p["fee_gamma"] + + +def test_commit_accept_allowed_extra_profit(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["allowed_extra_profit"] = 10**17 + with boa.env.prank(factory_admin): + _apply_new_params(swap, p) + + allowed_extra_profit = swap.internal._unpack_3( + swap._storage.packed_rebalancing_params.get() + )[0] + assert allowed_extra_profit == p["allowed_extra_profit"] + + +def test_commit_accept_adjustment_step(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["adjustment_step"] = 10**17 + with boa.env.prank(factory_admin): + _apply_new_params(swap, p) + + adjustment_step = swap.internal._unpack_3( + swap._storage.packed_rebalancing_params.get() + )[1] + assert adjustment_step == p["adjustment_step"] + + +def test_commit_accept_ma_time(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["ma_time"] = 872 + with boa.env.prank(factory_admin): + _apply_new_params(swap, p) + + ma_time = swap.internal._unpack_3( + swap._storage.packed_rebalancing_params.get() + )[2] + assert ma_time == p["ma_time"] + + +def test_commit_accept_xcp_ma_time(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["xcp_ma_time"] = 872541 + with boa.env.prank(factory_admin): + _apply_new_params(swap, p) + + assert swap.xcp_ma_time() == p["xcp_ma_time"] + + +def test_commit_accept_rebalancing_params(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["allowed_extra_profit"] = 10**17 + p["adjustment_step"] = 10**17 + p["ma_time"] = 1000 + + with boa.env.prank(factory_admin): + _apply_new_params(swap, p) + + rebalancing_params = swap.internal._unpack_3( + swap._storage.packed_rebalancing_params.get() + ) + assert rebalancing_params[0] == p["allowed_extra_profit"] + assert rebalancing_params[1] == p["adjustment_step"] + assert rebalancing_params[2] == p["ma_time"] diff --git a/tests/unitary/pool/admin/test_ramp_A_gamma.py b/tests/unitary/pool/admin/test_ramp_A_gamma.py new file mode 100644 index 00000000..f06126ca --- /dev/null +++ b/tests/unitary/pool/admin/test_ramp_A_gamma.py @@ -0,0 +1,49 @@ +import copy + +import boa + + +def test_ramp_A_gamma_up(swap, factory_admin, params): + + p = copy.deepcopy(params) + future_A = p["A"] + 10000 + future_gamma = p["gamma"] + 10000 + future_time = boa.env.vm.state.timestamp + 86400 + + initial_A_gamma = [swap.A(), swap.gamma()] + swap.ramp_A_gamma( + future_A, future_gamma, future_time, sender=factory_admin + ) + + boa.env.time_travel(10000) + current_A_gamma = [swap.A(), swap.gamma()] + for i in range(2): + assert current_A_gamma[i] > initial_A_gamma[i] + + boa.env.time_travel(76400) + current_A_gamma = [swap.A(), swap.gamma()] + assert current_A_gamma[0] == future_A + assert current_A_gamma[1] == future_gamma + + +def test_ramp_A_gamma_down(swap, factory_admin, params): + + p = copy.deepcopy(params) + future_A = p["A"] - 10000 + future_gamma = p["gamma"] - 10000 + future_time = boa.env.vm.state.timestamp + 86400 + + initial_A_gamma = [swap.A(), swap.gamma()] + swap.ramp_A_gamma( + future_A, future_gamma, future_time, sender=factory_admin + ) + + boa.env.time_travel(10000) + current_A_gamma = [swap.A(), swap.gamma()] + for i in range(2): + assert current_A_gamma[i] < initial_A_gamma[i] + + boa.env.time_travel(76400) + current_A_gamma = [swap.A(), swap.gamma()] + assert current_A_gamma[0] == future_A + assert current_A_gamma[1] == future_gamma diff --git a/tests/unitary/pool/admin/test_revert_commit_params.py b/tests/unitary/pool/admin/test_revert_commit_params.py new file mode 100644 index 00000000..48228166 --- /dev/null +++ b/tests/unitary/pool/admin/test_revert_commit_params.py @@ -0,0 +1,81 @@ +import copy + +import boa + + +def _apply_new_params(swap, params): + swap.apply_new_parameters( + params["mid_fee"], + params["out_fee"], + params["fee_gamma"], + params["allowed_extra_profit"], + params["adjustment_step"], + params["ma_time"], + params["xcp_ma_time"], + ) + + +def test_commit_incorrect_fee_params(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["mid_fee"] = p["out_fee"] + 1 + with boa.env.prank(factory_admin): + with boa.reverts("mid-fee is too high"): + _apply_new_params(swap, p) + + p["out_fee"] = 0 + with boa.reverts("fee is out of range"): + _apply_new_params(swap, p) + + # too large out_fee revert to old out_fee: + p["mid_fee"] = params["mid_fee"] + p["out_fee"] = 10**10 + 1 # <-- MAX_FEE + _apply_new_params(swap, p) + logs = swap.get_logs()[0] + assert logs.args[1] == params["out_fee"] + + +def test_commit_incorrect_fee_gamma(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["fee_gamma"] = 0 + + with boa.env.prank(factory_admin): + + with boa.reverts("fee_gamma out of range [1 .. 10**18]"): + _apply_new_params(swap, p) + + p["fee_gamma"] = 10**18 + 1 + _apply_new_params(swap, p) + + # it will not change fee_gamma as it is above 10**18 + assert swap.get_logs()[0].args[2] == params["fee_gamma"] + + +def test_commit_rebalancing_params(swap, factory_admin, params): + + p = copy.deepcopy(params) + p["allowed_extra_profit"] = 10**18 + 1 + p["adjustment_step"] == 10**18 + 1 + p["ma_time"] = 872542 + 1 + + with boa.env.prank(factory_admin): + + with boa.env.anchor(): + _apply_new_params(swap, p) + logs = swap.get_logs()[0] + + # values revert to contract's storage values: + assert logs.args[3] == params["allowed_extra_profit"] + assert logs.args[4] == params["adjustment_step"] + assert logs.args[5] == params["ma_time"] + + with boa.reverts("MA time should be longer than 60/ln(2)"): + p["ma_time"] = 86 + _apply_new_params(swap, p) + + +def test_revert_unauthorised_commit(swap, user, params): + + with boa.env.prank(user), boa.reverts(dev="only owner"): + _apply_new_params(swap, params) diff --git a/tests/unitary/pool/admin/test_revert_ramp.py b/tests/unitary/pool/admin/test_revert_ramp.py new file mode 100644 index 00000000..e485c628 --- /dev/null +++ b/tests/unitary/pool/admin/test_revert_ramp.py @@ -0,0 +1,41 @@ +import boa + + +def test_revert_unauthorised_ramp(swap, user): + + with boa.env.prank(user), boa.reverts(dev="only owner"): + swap.ramp_A_gamma(1, 1, 1) + + +def test_revert_ramp_while_ramping(swap, factory_admin): + + assert swap.initial_A_gamma_time() == 0 + + A_gamma = [swap.A(), swap.gamma()] + future_time = boa.env.vm.state.timestamp + 86400 + 1 + with boa.env.prank(factory_admin): + swap.ramp_A_gamma(A_gamma[0] + 1, A_gamma[1] + 1, future_time) + + with boa.reverts(dev="ramp undergoing"): + swap.ramp_A_gamma(A_gamma[0], A_gamma[1], future_time) + + +def test_revert_fast_ramps(swap, factory_admin): + + A_gamma = [swap.A(), swap.gamma()] + future_time = boa.env.vm.state.timestamp + 10 + with boa.env.prank(factory_admin), boa.reverts(dev="insufficient time"): + swap.ramp_A_gamma(A_gamma[0] + 1, A_gamma[1] + 1, future_time) + + +def test_revert_unauthorised_stop_ramp(swap, factory_admin, user): + + assert swap.initial_A_gamma_time() == 0 + + A_gamma = [swap.A(), swap.gamma()] + future_time = boa.env.vm.state.timestamp + 86400 + 1 + with boa.env.prank(factory_admin): + swap.ramp_A_gamma(A_gamma[0] + 1, A_gamma[1] + 1, future_time) + + with boa.env.prank(user), boa.reverts(dev="only owner"): + swap.stop_ramp_A_gamma() diff --git a/tests/unitary/pool/stateful/stateful_base.py b/tests/unitary/pool/stateful/stateful_base.py index 6cec0a29..31900774 100644 --- a/tests/unitary/pool/stateful/stateful_base.py +++ b/tests/unitary/pool/stateful/stateful_base.py @@ -1,51 +1,62 @@ +import contextlib from math import log +import boa from boa.test import strategy +from hypothesis.stateful import RuleBasedStateMachine, invariant, rule + +from tests.fixtures.pool import INITIAL_PRICES +from tests.utils.tokens import mint_for_testing MAX_SAMPLES = 20 MAX_D = 10**12 * 10**18 # $1T is hopefully a reasonable cap for tests -INITIAL_PRICES = [10**18, 1500 * 10**18] # price relative to coin_id = 0 -class StatefulBase: +class StatefulBase(RuleBasedStateMachine): exchange_amount_in = strategy("uint256", max_value=10**9 * 10**18) exchange_i = strategy("uint8", max_value=1) sleep_time = strategy("uint256", max_value=86400 * 7) user = strategy("address") - def __init__(self, accounts, coins, crypto_swap, token): - self.accounts = accounts - self.swap = crypto_swap - self.coins = coins - self.token = token + def __init__(self): + + super().__init__() - def setup(self, user_id=0): self.decimals = [int(c.decimals()) for c in self.coins] - self.user_balances = {u: [0] * 2 for u in self.accounts} + self.user_balances = {u: [0] * 2 for u in self.users} + self.initial_prices = INITIAL_PRICES self.initial_deposit = [ 10**4 * 10 ** (18 + d) // p - for p, d in zip([10**18] + INITIAL_PRICES, self.decimals) + for p, d in zip(self.initial_prices, self.decimals) ] # $10k * 2 - self.initial_prices = [10**18] + INITIAL_PRICES - user = self.accounts[user_id] - for coin, q in zip(self.coins, self.initial_deposit): - coin._mint_for_testing(user, q) - coin.approve(self.swap, 2**256 - 1, {"from": user}) + self.xcp_profit = 10**18 + self.xcp_profit_a = 10**18 + + self.total_supply = 0 + self.previous_pool_profit = 0 + + self.swap_admin = self.swap.admin() + self.fee_receiver = self.swap.fee_receiver() + + for user in self.users: + for coin in self.coins: + coin.approve(self.swap, 2**256 - 1, sender=user) - # Inf approve all, too. Not always that's the best way though - for u in self.accounts: - if u != user: - for coin in self.coins: - coin.approve(self.swap, 2**256 - 1, {"from": u}) + self.setup() + + def setup(self, user_id=0): + + user = self.users[user_id] + for coin, q in zip(self.coins, self.initial_deposit): + mint_for_testing(coin, user, q) # Very first deposit - self.swap.add_liquidity(self.initial_deposit, 0, {"from": user}) + self.swap.add_liquidity(self.initial_deposit, 0, sender=user) self.balances = self.initial_deposit[:] - self.total_supply = self.token.balanceOf(user) + self.total_supply = self.swap.balanceOf(user) self.xcp_profit = 10**18 - print(" \n ----------- INIT ----------------- ") def convert_amounts(self, amounts): prices = [10**18] + [self.swap.price_scale()] @@ -91,10 +102,19 @@ def check_limits(self, amounts, D=True, y=True): return True - def rule_exchange(self, exchange_amount_in, exchange_i, user): - return self._rule_exchange(exchange_amount_in, exchange_i, user) + @rule( + exchange_amount_in=exchange_amount_in, + exchange_i=exchange_i, + user=user, + ) + def exchange(self, exchange_amount_in, exchange_i, user): + out = self._exchange(exchange_amount_in, exchange_i, user) + if out: + self.swap_out = out + return + self.swap_out = None - def _rule_exchange( + def _exchange( self, exchange_amount_in, exchange_i, user, check_out_amount=True ): exchange_j = 1 - exchange_i @@ -107,14 +127,17 @@ def _rule_exchange( _amounts[exchange_i] = exchange_amount_in if self.check_limits(_amounts) and exchange_amount_in > 10000: raise - return False - self.coins[exchange_i]._mint_for_testing(user, exchange_amount_in) + return None + mint_for_testing(self.coins[exchange_i], user, exchange_amount_in) d_balance_i = self.coins[exchange_i].balanceOf(user) d_balance_j = self.coins[exchange_j].balanceOf(user) try: - self.swap.exchange( - exchange_i, exchange_j, exchange_amount_in, 0, {"from": user} + self.coins[exchange_i].approve( + self.swap, 2**256 - 1, sender=user + ) + out = self.swap.exchange( + exchange_i, exchange_j, exchange_amount_in, 0, sender=user ) except Exception: # Small amounts may fail with rounding errors @@ -125,7 +148,7 @@ def _rule_exchange( and exchange_amount_in / self.swap.balances(exchange_i) > 1e-13 ): raise - return False + return None # This is to check that we didn't end up in a borked state after # an exchange succeeded @@ -134,7 +157,7 @@ def _rule_exchange( exchange_i, 10**16 * 10 ** self.decimals[exchange_j] - // ([10**18] + INITIAL_PRICES)[exchange_j], + // INITIAL_PRICES[exchange_j], ) d_balance_i -= self.coins[exchange_i].balanceOf(user) @@ -154,22 +177,26 @@ def _rule_exchange( self.balances[exchange_i] += d_balance_i self.balances[exchange_j] += d_balance_j - return True + return out - def rule_sleep(self, sleep_time): - self.chain.sleep(sleep_time) + @rule(sleep_time=sleep_time) + def sleep(self, sleep_time): + boa.env.time_travel(sleep_time) - def invariant_balances(self): + @invariant() + def balances(self): balances = [self.swap.balances(i) for i in range(2)] balances_of = [c.balanceOf(self.swap) for c in self.coins] for i in range(2): assert self.balances[i] == balances[i] assert self.balances[i] == balances_of[i] - def invariant_total_supply(self): - assert self.total_supply == self.token.totalSupply() + @invariant() + def total_supply(self): + assert self.total_supply == self.swap.totalSupply() - def invariant_virtual_price(self): + @invariant() + def virtual_price(self): virtual_price = self.swap.virtual_price() xcp_profit = self.swap.xcp_profit() get_virtual_price = self.swap.get_virtual_price() @@ -187,4 +214,57 @@ def invariant_virtual_price(self): assert abs(log(virtual_price / get_virtual_price)) < 1e-10 self.xcp_profit = xcp_profit - print("INVARIANT UPDATED xcp_profit", self.xcp_profit) + + @invariant() + def up_only_profit(self): + + current_profit = xcp_profit = self.swap.xcp_profit() + xcp_profit_a = self.swap.xcp_profit_a() + current_profit = (xcp_profit + xcp_profit_a + 1) // 2 + + assert current_profit >= self.previous_pool_profit + self.previous_pool_profit = current_profit + + @contextlib.contextmanager + def upkeep_on_claim(self): + + admin_balances_pre = [ + c.balanceOf(self.fee_receiver) for c in self.coins + ] + pool_is_ramping = ( + self.swap.future_A_gamma_time() > boa.env.vm.state.timestamp + ) + + try: + + yield + + finally: + + new_xcp_profit_a = self.swap.xcp_profit_a() + old_xcp_profit_a = self.xcp_profit_a + + claimed = False + if new_xcp_profit_a > old_xcp_profit_a: + claimed = True + self.xcp_profit_a = new_xcp_profit_a + + admin_balances_post = [ + c.balanceOf(self.fee_receiver) for c in self.coins + ] + + if claimed: + + for i in range(2): + claimed_amount = ( + admin_balances_post[i] - admin_balances_pre[i] + ) + assert ( + claimed_amount > 0 + ) # check if non zero amounts of claim + assert not pool_is_ramping # cannot claim while ramping + + # update self.balances + self.balances[i] -= claimed_amount + + self.xcp_profit = self.swap.xcp_profit() diff --git a/tests/unitary/pool/stateful/test_add_liquidity.py b/tests/unitary/pool/stateful/test_add_liquidity.py deleted file mode 100644 index 85fd488f..00000000 --- a/tests/unitary/pool/stateful/test_add_liquidity.py +++ /dev/null @@ -1,38 +0,0 @@ -# flake8: noqa - -import math - -import boa -import pytest -from boa.test import strategy -from hypothesis import given, settings - -from tests.fixtures.pool import INITIAL_PRICES -from tests.utils import simulation_int_many as sim -from tests.utils.tokens import mint_for_testing - -SETTINGS = {"max_examples": 100, "deadline": None} - - -@pytest.fixture(scope="module") -def test_deposit(swap, coins, user, fee_receiver): - - quantities = [10**36 // p for p in INITIAL_PRICES] # $3M worth - - for coin, q in zip(coins, quantities): - mint_for_testing(coin, user, q) - with boa.env.prank(user): - coin.approve(swap, 2**256 - 1) - - bal_before = boa.env.get_balance(swap.address) - with boa.env.prank(user): - swap.add_liquidity(quantities, 0) - - # test if eth wasnt deposited: - assert boa.env.get_balance(swap.address) == bal_before - - token_balance = swap.balanceOf(user) - assert ( - token_balance == swap.totalSupply() - swap.balanceOf(fee_receiver) > 0 - ) - assert abs(swap.get_virtual_price() / 1e18 - 1) < 1e-3 diff --git a/tests/unitary/pool/stateful/test_multiprecision.py b/tests/unitary/pool/stateful/test_multiprecision.py new file mode 100644 index 00000000..dcecf832 --- /dev/null +++ b/tests/unitary/pool/stateful/test_multiprecision.py @@ -0,0 +1,54 @@ +import pytest +from boa.test import strategy +from hypothesis.stateful import rule, run_state_machine_as_test + +from tests.unitary.pool.stateful.test_stateful import NumbaGoUp + +MAX_SAMPLES = 100 +STEP_COUNT = 100 + + +@pytest.fixture(scope="module") +def coins(stgusdc): + return stgusdc + + +@pytest.fixture(scope="module") +def swap(swap_multiprecision): + return swap_multiprecision + + +class Multiprecision(NumbaGoUp): + exchange_amount_in = strategy( + "uint256", min_value=10**18, max_value=50000 * 10**18 + ) + user = strategy("address") + exchange_i = strategy("uint8", max_value=1) + + @rule( + exchange_amount_in=exchange_amount_in, + exchange_i=exchange_i, + user=user, + ) + def exchange(self, exchange_amount_in, exchange_i, user): + exchange_amount_in = exchange_amount_in // 10 ** ( + 18 - self.decimals[exchange_i] + ) + super().exchange(exchange_amount_in, exchange_i, user) + + +def test_multiprecision(users, coins, swap): + from hypothesis import settings + from hypothesis._settings import HealthCheck + + Multiprecision.TestCase.settings = settings( + max_examples=MAX_SAMPLES, + stateful_step_count=STEP_COUNT, + suppress_health_check=HealthCheck.all(), + deadline=None, + ) + + for k, v in locals().items(): + setattr(Multiprecision, k, v) + + run_state_machine_as_test(Multiprecision) diff --git a/tests/unitary/pool/stateful/test_ramp.py b/tests/unitary/pool/stateful/test_ramp.py new file mode 100644 index 00000000..ab62ac9a --- /dev/null +++ b/tests/unitary/pool/stateful/test_ramp.py @@ -0,0 +1,107 @@ +import boa +from boa.test import strategy +from hypothesis.stateful import invariant, rule, run_state_machine_as_test + +from tests.unitary.pool.stateful.test_stateful import NumbaGoUp + +MAX_SAMPLES = 20 +STEP_COUNT = 100 +MAX_D = 10**12 * 10**18 # $1T is hopefully a reasonable cap for tests +ALLOWED_DIFFERENCE = 0.001 + + +class RampTest(NumbaGoUp): + check_out_amount = strategy("bool") + exchange_amount_in = strategy( + "uint256", min_value=10**18, max_value=50000 * 10**18 + ) + token_amount = strategy( + "uint256", min_value=10**18, max_value=10**12 * 10**18 + ) + deposit_amounts = strategy( + "uint256[3]", min_value=10**18, max_value=10**9 * 10**18 + ) + user = strategy("address") + exchange_i = strategy("uint8", max_value=1) + + def setup(self, user_id=0): + super().setup(user_id) + new_A = self.swap.A() * 2 + new_gamma = self.swap.gamma() * 2 + self.swap.ramp_A_gamma( + new_A, + new_gamma, + boa.env.vm.state.timestamp + 14 * 86400, + sender=self.swap_admin, + ) + + @rule(user=user, deposit_amounts=deposit_amounts) + def deposit(self, deposit_amounts, user): + deposit_amounts[1:] = [ + deposit_amounts[0], + deposit_amounts[1] * 10**18 // self.swap.price_oracle(), + ] + super().deposit(deposit_amounts, user) + + @rule( + user=user, + exchange_i=exchange_i, + exchange_amount_in=exchange_amount_in, + check_out_amount=check_out_amount, + ) + def exchange(self, exchange_amount_in, exchange_i, user, check_out_amount): + + if exchange_i > 0: + exchange_amount_in = ( + exchange_amount_in * 10**18 // self.swap.price_oracle() + ) + if exchange_amount_in < 1000: + return + + super()._exchange( + exchange_amount_in, + exchange_i, + user, + ALLOWED_DIFFERENCE if check_out_amount else False, + ) + + @rule( + user=user, + token_amount=token_amount, + exchange_i=exchange_i, + check_out_amount=check_out_amount, + ) + def remove_liquidity_one_coin( + self, token_amount, exchange_i, user, check_out_amount + ): + + if check_out_amount: + super().remove_liquidity_one_coin( + token_amount, exchange_i, user, ALLOWED_DIFFERENCE + ) + else: + super().remove_liquidity_one_coin( + token_amount, exchange_i, user, False + ) + + @invariant() + def virtual_price(self): + # Invariant is not conserved here + pass + + +def test_ramp(users, coins, swap): + from hypothesis import settings + from hypothesis._settings import HealthCheck + + RampTest.TestCase.settings = settings( + max_examples=MAX_SAMPLES, + stateful_step_count=STEP_COUNT, + suppress_health_check=HealthCheck.all(), + deadline=None, + ) + + for k, v in locals().items(): + setattr(RampTest, k, v) + + run_state_machine_as_test(RampTest) diff --git a/tests/unitary/pool/stateful/test_ramp_nocheck.py b/tests/unitary/pool/stateful/test_ramp_nocheck.py new file mode 100644 index 00000000..2519fbc4 --- /dev/null +++ b/tests/unitary/pool/stateful/test_ramp_nocheck.py @@ -0,0 +1,81 @@ +import boa +from boa.test import strategy +from hypothesis.stateful import invariant, rule, run_state_machine_as_test + +from tests.unitary.pool.stateful.test_stateful import NumbaGoUp + +MAX_SAMPLES = 20 +STEP_COUNT = 100 +MAX_D = 10**12 * 10**18 # $1T is hopefully a reasonable cap for tests +ALLOWED_DIFFERENCE = 0.02 + + +class RampTest(NumbaGoUp): + future_gamma = strategy( + "uint256", + min_value=int(2.8e-4 * 1e18 / 9), + max_value=int(2.8e-4 * 1e18 * 9), + ) + future_A = strategy( + "uint256", + min_value=90 * 2**2 * 10000 // 9, + max_value=90 * 2**2 * 10000 * 9, + ) + check_out_amount = strategy("bool") + exchange_amount_in = strategy( + "uint256", min_value=10**18, max_value=50000 * 10**18 + ) + token_amount = strategy( + "uint256", min_value=10**18, max_value=10**12 * 10**18 + ) + user = strategy("address") + exchange_i = strategy("uint8", max_value=1) + + def initialize(self, future_A, future_gamma): + self.swap.ramp_A_gamma( + future_A, + future_gamma, + boa.env.vm.state.timestamp + 14 * 86400, + sender=self.swap_admin, + ) + + @rule( + exchange_amount_in=exchange_amount_in, + exchange_i=exchange_i, + user=user, + ) + def exchange(self, exchange_amount_in, exchange_i, user): + try: + super()._exchange(exchange_amount_in, exchange_i, user, False) + except Exception: + if exchange_amount_in > 10**9: + # Small swaps can fail at ramps + raise + + @rule(token_amount=token_amount, exchange_i=exchange_i, user=user) + def remove_liquidity_one_coin(self, token_amount, exchange_i, user): + super().remove_liquidity_one_coin( + token_amount, exchange_i, user, False + ) + + @invariant() + def virtual_price(self): + # Invariant is not conserved here + pass + + +def test_ramp(users, coins, swap): + from hypothesis import settings + from hypothesis._settings import HealthCheck + + RampTest.TestCase.settings = settings( + max_examples=MAX_SAMPLES, + stateful_step_count=STEP_COUNT, + suppress_health_check=HealthCheck.all(), + deadline=None, + ) + + for k, v in locals().items(): + setattr(RampTest, k, v) + + run_state_machine_as_test(RampTest) diff --git a/tests/unitary/pool/stateful/test_simulate.py b/tests/unitary/pool/stateful/test_simulate.py new file mode 100644 index 00000000..53dc3876 --- /dev/null +++ b/tests/unitary/pool/stateful/test_simulate.py @@ -0,0 +1,133 @@ +from math import log + +import boa +from boa.test import strategy +from hypothesis.stateful import invariant, rule, run_state_machine_as_test + +from tests.unitary.pool.stateful.stateful_base import StatefulBase +from tests.utils import simulation_int_many as sim +from tests.utils.tokens import mint_for_testing + +MAX_SAMPLES = 20 +STEP_COUNT = 100 + + +def approx(x1, x2, precision): + return abs(log(x1 / x2)) <= precision + + +def logdiff(x1, x2): + return abs(log(x1 / x2)) + + +class StatefulSimulation(StatefulBase): + exchange_amount_in = strategy( + "uint256", min_value=10**17, max_value=10**5 * 10**18 + ) + exchange_i = strategy("uint8", max_value=1) + user = strategy("address") + + def setup(self): + + super().setup() + + for u in self.users[1:]: + for coin, q in zip(self.coins, self.initial_deposit): + mint_for_testing(coin, u, q) + for i in range(2): + self.balances[i] += self.initial_deposit[i] + self.swap.add_liquidity(self.initial_deposit, 0, sender=u) + self.total_supply += self.swap.balanceOf(u) + + self.virtual_price = self.swap.get_virtual_price() + + self.trader = sim.Trader( + self.swap.A(), + self.swap.gamma(), + self.swap.D(), + 2, + [10**18, self.swap.price_scale()], + self.swap.mid_fee() / 1e10, + self.swap.out_fee() / 1e10, + self.swap.allowed_extra_profit(), + self.swap.fee_gamma(), + self.swap.adjustment_step() / 1e18, + int( + self.swap.ma_time() / 0.693 + ), # crypto swap returns ma time in sec + ) + for i in range(2): + self.trader.curve.x[i] = self.swap.balances(i) + + # Adjust virtual prices + self.trader.xcp_profit = self.swap.xcp_profit() + self.trader.xcp_profit_real = self.swap.virtual_price() + self.trader.t = boa.env.vm.state.timestamp + self.swap_no = 0 + + @rule( + exchange_amount_in=exchange_amount_in, + exchange_i=exchange_i, + user=user, + ) + def exchange(self, exchange_amount_in, exchange_i, user): + + dx = ( + exchange_amount_in + * 10**18 + // self.trader.price_oracle[exchange_i] + ) + self.swap_no += 1 + super().exchange(dx, exchange_i, user) + + if not self.swap_out: + return # if swap breaks, dont check. + + dy_trader = self.trader.buy(dx, exchange_i, 1 - exchange_i) + self.trader.tweak_price(boa.env.vm.state.timestamp) + + # exchange checks: + out_logdiff = logdiff(self.swap_out, dy_trader) + price_oracle_logdiff = logdiff( + self.swap.price_oracle(), self.trader.price_oracle[1] + ) + + assert out_logdiff <= 1e-3 + assert price_oracle_logdiff <= 1e-3 + + boa.env.time_travel(12) + + @invariant() + def simulator(self): + if self.trader.xcp_profit / 1e18 - 1 > 1e-8: + assert ( + abs(self.trader.xcp_profit - self.swap.xcp_profit()) + / (self.trader.xcp_profit - 10**18) + < 0.05 + ) + + price_scale = self.swap.price_scale() + price_trader = self.trader.curve.p[1] + price_scale_logdiff = logdiff(price_scale, price_trader) + try: + assert price_scale_logdiff <= 1e-3 + except Exception: + if self.check_limits([0, 0, 0]): + assert False + + +def test_sim(users, coins, swap): + from hypothesis import settings + from hypothesis._settings import HealthCheck + + StatefulSimulation.TestCase.settings = settings( + max_examples=MAX_SAMPLES, + stateful_step_count=STEP_COUNT, + suppress_health_check=HealthCheck.all(), + deadline=None, + ) + + for k, v in locals().items(): + setattr(StatefulSimulation, k, v) + + run_state_machine_as_test(StatefulSimulation) diff --git a/tests/unitary/pool/stateful/test_stateful.py b/tests/unitary/pool/stateful/test_stateful.py new file mode 100644 index 00000000..8f9653ce --- /dev/null +++ b/tests/unitary/pool/stateful/test_stateful.py @@ -0,0 +1,182 @@ +import boa +from boa.test import strategy +from hypothesis.stateful import rule, run_state_machine_as_test + +from tests.fixtures.pool import INITIAL_PRICES +from tests.unitary.pool.stateful.stateful_base import StatefulBase +from tests.utils.tokens import mint_for_testing + +MAX_SAMPLES = 20 +STEP_COUNT = 100 +MAX_D = 10**12 * 10**18 # $1T is hopefully a reasonable cap for tests + + +class NumbaGoUp(StatefulBase): + """ + Test that profit goes up + """ + + user = strategy("address") + exchange_i = strategy("uint8", max_value=1) + deposit_amounts = strategy( + "uint256[2]", min_value=0, max_value=10**9 * 10**18 + ) + token_amount = strategy("uint256", max_value=10**12 * 10**18) + check_out_amount = strategy("bool") + + @rule(deposit_amounts=deposit_amounts, user=user) + def deposit(self, deposit_amounts, user): + + if self.swap.D() > MAX_D: + return + + amounts = self.convert_amounts(deposit_amounts) + if sum(amounts) == 0: + return + + new_balances = [x + y for x, y in zip(self.balances, amounts)] + + for coin, q in zip(self.coins, amounts): + mint_for_testing(coin, user, q) + + try: + + tokens = self.swap.balanceOf(user) + self.swap.add_liquidity(amounts, 0, sender=user) + tokens = self.swap.balanceOf(user) - tokens + self.total_supply += tokens + self.balances = new_balances + + except Exception: + + if self.check_limits(amounts): + raise + return + + # This is to check that we didn't end up in a borked state after + # an exchange succeeded + try: + self.swap.get_dy(0, 1, 10 ** (self.decimals[0] - 2)) + except Exception: + self.swap.get_dy( + 1, + 0, + 10**16 * 10 ** self.decimals[1] // self.swap.price_scale(), + ) + + @rule(token_amount=token_amount, user=user) + def remove_liquidity(self, token_amount, user): + if self.swap.balanceOf(user) < token_amount or token_amount == 0: + with boa.reverts(): + self.swap.remove_liquidity(token_amount, [0] * 2, sender=user) + else: + amounts = [c.balanceOf(user) for c in self.coins] + tokens = self.swap.balanceOf(user) + with self.upkeep_on_claim(): + self.swap.remove_liquidity(token_amount, [0] * 2, sender=user) + tokens -= self.swap.balanceOf(user) + self.total_supply -= tokens + amounts = [ + (c.balanceOf(user) - a) for c, a in zip(self.coins, amounts) + ] + self.balances = [b - a for a, b in zip(amounts, self.balances)] + + # Virtual price resets if everything is withdrawn + if self.total_supply == 0: + self.virtual_price = 10**18 + + @rule( + token_amount=token_amount, + exchange_i=exchange_i, + user=user, + check_out_amount=check_out_amount, + ) + def remove_liquidity_one_coin( + self, token_amount, exchange_i, user, check_out_amount + ): + + try: + calc_out_amount = self.swap.calc_withdraw_one_coin( + token_amount, exchange_i + ) + except Exception: + if ( + self.check_limits([0] * 2) + and not (token_amount > self.total_supply) + and token_amount > 10000 + ): + self.swap.calc_withdraw_one_coin( + token_amount, exchange_i, sender=user + ) + return + + d_token = self.swap.balanceOf(user) + if d_token < token_amount: + with boa.reverts(): + self.swap.remove_liquidity_one_coin( + token_amount, exchange_i, 0, sender=user + ) + return + + d_balance = self.coins[exchange_i].balanceOf(user) + try: + with self.upkeep_on_claim(): + self.swap.remove_liquidity_one_coin( + token_amount, exchange_i, 0, sender=user + ) + except Exception: + # Small amounts may fail with rounding errors + if ( + calc_out_amount > 100 + and token_amount / self.total_supply > 1e-10 + and calc_out_amount / self.swap.balances(exchange_i) > 1e-10 + ): + raise + return + + # This is to check that we didn't end up in a borked state after + # an exchange succeeded + _deposit = [0] * 2 + _deposit[exchange_i] = ( + 10**16 + * 10 ** self.decimals[exchange_i] + // ([10**18] + INITIAL_PRICES)[exchange_i] + ) + assert self.swap.calc_token_amount(_deposit, True) + + d_balance = self.coins[exchange_i].balanceOf(user) - d_balance + d_token = d_token - self.swap.balanceOf(user) + + if check_out_amount: + if check_out_amount is True: + assert ( + calc_out_amount == d_balance + ), f"{calc_out_amount} vs {d_balance} for {token_amount}" + else: + assert abs(calc_out_amount - d_balance) <= max( + check_out_amount * calc_out_amount, 5 + ), f"{calc_out_amount} vs {d_balance} for {token_amount}" + + self.balances[exchange_i] -= d_balance + self.total_supply -= d_token + + # Virtual price resets if everything is withdrawn + if self.total_supply == 0: + self.virtual_price = 10**18 + + +def test_numba_go_up(users, coins, swap): + from hypothesis import settings + from hypothesis._settings import HealthCheck + + NumbaGoUp.TestCase.settings = settings( + max_examples=MAX_SAMPLES, + stateful_step_count=STEP_COUNT, + suppress_health_check=HealthCheck.all(), + deadline=None, + ) + + for k, v in locals().items(): + setattr(NumbaGoUp, k, v) + + run_state_machine_as_test(NumbaGoUp) diff --git a/tests/unitary/pool/test_exchange_received.py b/tests/unitary/pool/test_exchange_received.py deleted file mode 100644 index de2ab754..00000000 --- a/tests/unitary/pool/test_exchange_received.py +++ /dev/null @@ -1,33 +0,0 @@ -# flake8: noqa -import boa -from boa.test import strategy -from hypothesis import given, settings # noqa - -from tests.fixtures.pool import INITIAL_PRICES -from tests.utils.tokens import mint_for_testing - -SETTINGS = {"max_examples": 100, "deadline": None} - - -@given( - amount=strategy( - "uint256", min_value=10**10, max_value=10**6 * 10**18 - ), - split_in=strategy("uint256", min_value=0, max_value=100), - split_out=strategy("uint256", min_value=0, max_value=100), - i=strategy("uint", min_value=0, max_value=1), - j=strategy("uint", min_value=0, max_value=1), -) -@settings(**SETTINGS) -def test_exchange_received( - swap_with_deposit, - views_contract, - coins, - user, - amount, - split_in, - split_out, - i, - j, -): - pass diff --git a/tests/unitary/pool/test_oracles.py b/tests/unitary/pool/test_oracles.py new file mode 100644 index 00000000..c3808777 --- /dev/null +++ b/tests/unitary/pool/test_oracles.py @@ -0,0 +1,222 @@ +from math import exp, log, log2, sqrt + +import boa +import pytest +from boa.test import strategy +from hypothesis import given, settings + +from tests.fixtures.pool import INITIAL_PRICES +from tests.utils.tokens import mint_for_testing + +SETTINGS = {"max_examples": 1000, "deadline": None} + + +def approx(x1, x2, precision): + return abs(log(x1 / x2)) <= precision + + +def norm(price_oracle, price_scale): + norm = 0 + ratio = price_oracle * 10**18 / price_scale + if ratio > 10**18: + ratio -= 10**18 + else: + ratio = 10**18 - ratio + norm += ratio**2 + return sqrt(norm) + + +def test_initial(swap_with_deposit): + assert swap_with_deposit.price_scale() == INITIAL_PRICES[1] + assert swap_with_deposit.price_oracle() == INITIAL_PRICES[1] + + +@given( + token_frac=strategy("uint256", min_value=10**6, max_value=10**16), + i=strategy("uint8", max_value=1), +) +@settings(**SETTINGS) +def test_last_price_remove_liq(swap_with_deposit, user, token_frac, i): + + prices = INITIAL_PRICES + token_amount = token_frac * swap_with_deposit.totalSupply() // 10**18 + + with boa.env.prank(user): + swap_with_deposit.remove_liquidity_one_coin(token_amount, i, 0) + + oracle_price = swap_with_deposit.last_prices() + assert abs(log2(oracle_price / prices[1])) < 0.1 + + +@given( + amount=strategy( + "uint256", min_value=10**10, max_value=2 * 10**6 * 10**18 + ), # Can be more than we have + i=strategy("uint8", min_value=0, max_value=1), + t=strategy("uint256", min_value=10, max_value=10 * 86400), +) +@settings(**SETTINGS) +def test_ma(swap_with_deposit, coins, user, amount, i, t): + + prices1 = INITIAL_PRICES + amount = amount * 10**18 // prices1[i] + mint_for_testing(coins[i], user, amount) + + rebal_params = swap_with_deposit.internal._unpack_3( + swap_with_deposit._storage.packed_rebalancing_params.get() + ) + ma_time = rebal_params[2] + + # here we dont mine because we're time travelling later + with boa.env.prank(user): + swap_with_deposit.exchange(i, 1 - i, amount, 0) + + prices2 = swap_with_deposit.last_prices() + boa.env.time_travel(t) + + with boa.env.prank(user): + swap_with_deposit.remove_liquidity_one_coin(10**15, 0, 0) + + prices3 = swap_with_deposit.price_oracle() + + # cap new price by 2x previous price oracle value: + new_price = min(prices2, 2 * prices1[1]) + + alpha = exp(-1 * t / ma_time) + theory = prices1[1] * alpha + new_price * (1 - alpha) + assert abs(log2(theory / prices3)) < 0.001 + + +@given( + amount=strategy( + "uint256", min_value=10**10, max_value=2 * 10**6 * 10**18 + ), # Can be more than we have + i=strategy("uint8", min_value=0, max_value=1), + t=strategy("uint256", min_value=10, max_value=10 * 86400), +) +@settings(**SETTINGS) +def test_xcp_ma(swap_with_deposit, coins, user, amount, i, t): + + price_scale = swap_with_deposit.price_scale() + D0 = swap_with_deposit.D() + xp = [0, 0] + xp[0] = D0 // 2 # N_COINS = 2 + xp[1] = D0 * 10**18 // (2 * price_scale) + + xcp0 = boa.eval(f"isqrt({xp[0]*xp[1]})") + + # after first deposit anf before any swaps: + # xcp oracle is equal to totalSupply + assert xcp0 == swap_with_deposit.totalSupply() + + amount = amount * 10**18 // INITIAL_PRICES[i] + mint_for_testing(coins[i], user, amount) + + ma_time = swap_with_deposit.xcp_ma_time() + + # swap to populate + with boa.env.prank(user): + swap_with_deposit.exchange(i, 1 - i, amount, 0) + + xcp1 = swap_with_deposit.last_xcp() + tvl = ( + swap_with_deposit.virtual_price() + * swap_with_deposit.totalSupply() + // 10**18 + ) + assert approx(xcp1, tvl, 1e-10) + + boa.env.time_travel(t) + + with boa.env.prank(user): + swap_with_deposit.remove_liquidity_one_coin(10**15, 0, 0) + + xcp2 = swap_with_deposit.xcp_oracle() + + alpha = exp(-1 * t / ma_time) + theory = xcp0 * alpha + xcp1 * (1 - alpha) + + assert approx(theory, xcp2, 1e-10) + + +# Sanity check for price scale +@given( + amount=strategy( + "uint256", min_value=10**10, max_value=2 * 10**6 * 10**18 + ), # Can be more than we have + i=strategy("uint8", min_value=0, max_value=1), + t=strategy("uint256", max_value=10 * 86400), +) +@settings(**SETTINGS) +def test_price_scale_range(swap_with_deposit, coins, user, amount, i, t): + + prices1 = INITIAL_PRICES + amount = amount * 10**18 // prices1[i] + mint_for_testing(coins[i], user, amount) + + with boa.env.prank(user): + swap_with_deposit.exchange(i, 1 - i, amount, 0) + + prices2 = swap_with_deposit.last_prices() + boa.env.time_travel(seconds=t) + + with boa.env.prank(user): + swap_with_deposit.remove_liquidity_one_coin(10**15, 0, 0) + + prices3 = swap_with_deposit.price_scale() + + if prices1[1] > prices2: + assert prices3 <= prices1[1] and prices3 >= prices2 + else: + assert prices3 >= prices1[1] and prices3 <= prices2 + + +@pytest.mark.parametrize("i", [0, 1]) +def test_price_scale_change(swap_with_deposit, i, coins, users): + j = 1 - i + amount = 10**6 * 10**18 + t = 86400 + user = users[1] + prices1 = INITIAL_PRICES + amount = amount * 10**18 // prices1[i] + mint_for_testing(coins[i], user, amount) + mint_for_testing(coins[j], user, amount) + coins[i].approve(swap_with_deposit, 2**256 - 1, sender=user) + coins[j].approve(swap_with_deposit, 2**256 - 1, sender=user) + + out = swap_with_deposit.exchange(i, j, amount, 0, sender=user) + swap_with_deposit.exchange(j, i, int(out * 0.95), 0, sender=user) + price_scale_1 = swap_with_deposit.price_scale() + + boa.env.time_travel(t) + + swap_with_deposit.exchange(0, 1, coins[0].balanceOf(user), 0, sender=user) + + price_oracle = swap_with_deposit.price_oracle() + rebal_params = swap_with_deposit.internal._unpack_3( + swap_with_deposit._storage.packed_rebalancing_params.get() + ) + _norm = norm(price_oracle, price_scale_1) + step = max(rebal_params[1], _norm / 5) + price_scale_2 = swap_with_deposit.price_scale() + + price_diff = abs(price_scale_2 - price_scale_1) + adjustment = int(step * abs(price_oracle - price_scale_1) / _norm) + assert price_diff > 0 + assert approx(adjustment, price_diff, 0.01) + assert approx( + swap_with_deposit.virtual_price(), + swap_with_deposit.get_virtual_price(), + 1e-10, + ) + + +def test_lp_price(swap_with_deposit): + tvl = ( + swap_with_deposit.balances(0) + + swap_with_deposit.balances(1) + * swap_with_deposit.price_scale() + // 10**18 + ) + naive_price = tvl * 10**18 // swap_with_deposit.totalSupply() + assert abs(swap_with_deposit.lp_price() / naive_price - 1) < 2e-3 diff --git a/tests/unitary/pool/token/conftest.py b/tests/unitary/pool/token/conftest.py new file mode 100644 index 00000000..f4149cb2 --- /dev/null +++ b/tests/unitary/pool/token/conftest.py @@ -0,0 +1,58 @@ +from copy import deepcopy + +import boa +import pytest +from eth_account import Account as EthAccount +from eth_account._utils.structured_data.hashing import ( + hash_domain, + hash_message, +) +from eth_account.messages import SignableMessage +from hexbytes import HexBytes + + +@pytest.fixture(scope="module") +def sign_permit(): + def _sign_permit(swap, owner, spender, value, deadline): + + PERMIT_STRUCT = { + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"}, + {"name": "salt", "type": "bytes32"}, + ], + "Permit": [ + {"name": "owner", "type": "address"}, + {"name": "spender", "type": "address"}, + {"name": "value", "type": "uint256"}, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + ], + }, + "primaryType": "Permit", + } + + struct = deepcopy(PERMIT_STRUCT) + struct["domain"] = dict( + name=swap.name(), + version=swap.version(), + chainId=boa.env.vm.chain_context.chain_id, + verifyingContract=swap.address, + salt=HexBytes(swap.salt()), + ) + struct["message"] = dict( + owner=owner.address, + spender=spender, + value=value, + nonce=swap.nonces(owner.address), + deadline=deadline, + ) + signable_message = SignableMessage( + b"\x01", hash_domain(struct), hash_message(struct) + ) + return EthAccount.sign_message(signable_message, owner._private_key) + + return _sign_permit diff --git a/tests/unitary/pool/token/test_approve.py b/tests/unitary/pool/token/test_approve.py new file mode 100644 index 00000000..fd4e8546 --- /dev/null +++ b/tests/unitary/pool/token/test_approve.py @@ -0,0 +1,81 @@ +import boa +import pytest + +from tests.utils.tokens import mint_for_testing + + +@pytest.mark.parametrize("idx", range(5)) +def test_initial_approval_is_zero(swap, alice, users, idx): + assert swap.allowance(alice, users[idx]) == 0 + + +def test_approve(swap, alice, bob): + + with boa.env.prank(alice): + swap.approve(bob, 10**19) + + assert swap.allowance(alice, bob) == 10**19 + + +def test_modify_approve_zero_nonzero(swap, alice, bob): + + with boa.env.prank(alice): + swap.approve(bob, 10**19) + swap.approve(bob, 0) + swap.approve(bob, 12345678) + + assert swap.allowance(alice, bob) == 12345678 + + +def test_revoke_approve(swap, alice, bob): + + with boa.env.prank(alice): + swap.approve(bob, 10**19) + swap.approve(bob, 0) + + assert swap.allowance(alice, bob) == 0 + + +def test_approve_self(swap, alice): + + with boa.env.prank(alice): + swap.approve(alice, 10**19) + + assert swap.allowance(alice, alice) == 10**19 + + +def test_only_affects_target(swap, alice, bob): + with boa.env.prank(alice): + swap.approve(bob, 10**19) + + assert swap.allowance(bob, alice) == 0 + + +def test_returns_true(swap, alice, bob): + with boa.env.prank(alice): + assert swap.approve(bob, 10**19) + + +def test_approval_event_fires(swap, alice, bob): + + with boa.env.prank(alice): + swap.approve(bob, 10**19) + + logs = swap.get_logs() + + assert len(logs) == 1 + assert logs[0].event_type.name == "Approval" + assert logs[0].topics[0].lower() == alice.lower() + assert logs[0].topics[1].lower() == bob.lower() + assert logs[0].args[0] == 10**19 + + +def test_infinite_approval(swap, alice, bob): + with boa.env.prank(alice): + swap.approve(bob, 2**256 - 1) + + mint_for_testing(swap, alice, 10**18) + with boa.env.prank(bob): + swap.transferFrom(alice, bob, 10**18) + + assert swap.allowance(alice, bob) == 2**256 - 1 diff --git a/tests/unitary/pool/token/test_permit.py b/tests/unitary/pool/token/test_permit.py new file mode 100644 index 00000000..83493b21 --- /dev/null +++ b/tests/unitary/pool/token/test_permit.py @@ -0,0 +1,92 @@ +import boa +from hexbytes import HexBytes + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + +# tests inspired by: +# https://github.com/yearn/yearn-vaults/blob/master/tests/functional/vault/test_permit.py # noqa: E501 +# https://github.com/curvefi/curve-stablecoin/blob/5b6708138d82419917328e8042f3857eac034796/tests/stablecoin/test_approve.py # noqa: E501 + + +def test_permit_success(eth_acc, bob, swap, sign_permit): + + value = 2**256 - 1 + deadline = boa.env.vm.state.timestamp + 600 + + sig = sign_permit( + swap=swap, + owner=eth_acc, + spender=bob, + value=value, + deadline=deadline, + ) + + assert swap.allowance(eth_acc.address, bob) == 0 + with boa.env.prank(bob): + assert swap.permit( + eth_acc.address, + bob, + value, + deadline, + sig.v, + HexBytes(sig.r), + HexBytes(sig.s), + ) + + logs = swap.get_logs() + + assert swap.allowance(eth_acc.address, bob) == value + assert len(logs) == 1 + assert swap.nonces(eth_acc.address) == 1 + assert logs[0].event_type.name == "Approval" + assert logs[0].topics[0].lower() == eth_acc.address.lower() + assert logs[0].topics[1].lower() == bob.lower() + assert logs[0].args[0] == value + + +def test_permit_reverts_owner_is_invalid(bob, swap): + with boa.reverts(dev="invalid owner"), boa.env.prank(bob): + swap.permit( + ZERO_ADDRESS, + bob, + 2**256 - 1, + boa.env.vm.state.timestamp + 600, + 27, + b"\x00" * 32, + b"\x00" * 32, + ) + + +def test_permit_reverts_deadline_is_invalid(bob, swap): + with boa.reverts(dev="permit expired"), boa.env.prank(bob): + swap.permit( + bob, + bob, + 2**256 - 1, + boa.env.vm.state.timestamp - 600, + 27, + b"\x00" * 32, + b"\x00" * 32, + ) + + +def test_permit_reverts_signature_is_invalid(bob, swap): + with boa.reverts(dev="invalid signature"), boa.env.prank(bob): + swap.permit( + bob, + bob, + 2**256 - 1, + boa.env.vm.state.timestamp + 600, + 27, + b"\x00" * 32, + b"\x00" * 32, + ) + + +def test_domain_separator_updates_when_chain_id_updates(swap): + + domain_separator = swap.DOMAIN_SEPARATOR() + with boa.env.anchor(): + boa.env.vm.patch.chain_id = 42 + assert domain_separator != swap.DOMAIN_SEPARATOR() diff --git a/tests/unitary/pool/token/test_transfer.py b/tests/unitary/pool/token/test_transfer.py new file mode 100644 index 00000000..4cbdb48f --- /dev/null +++ b/tests/unitary/pool/token/test_transfer.py @@ -0,0 +1,91 @@ +import boa + + +def test_sender_balance_decreases(loaded_alice, bob, swap): + sender_balance = swap.balanceOf(loaded_alice) + amount = sender_balance // 4 + + with boa.env.prank(loaded_alice): + swap.transfer(bob, amount) + + assert swap.balanceOf(loaded_alice) == sender_balance - amount + + +def test_receiver_balance_increases(loaded_alice, bob, swap): + receiver_balance = swap.balanceOf(bob) + amount = swap.balanceOf(loaded_alice) // 4 + + with boa.env.prank(loaded_alice): + swap.transfer(bob, amount) + + assert swap.balanceOf(bob) == receiver_balance + amount + + +def test_total_supply_not_affected(loaded_alice, bob, swap): + total_supply = swap.totalSupply() + amount = swap.balanceOf(loaded_alice) + + with boa.env.prank(loaded_alice): + swap.transfer(bob, amount) + + assert swap.totalSupply() == total_supply + + +def test_returns_true(loaded_alice, bob, swap): + amount = swap.balanceOf(loaded_alice) + + with boa.env.prank(loaded_alice): + assert swap.transfer(bob, amount) + + +def test_transfer_full_balance(loaded_alice, bob, swap): + amount = swap.balanceOf(loaded_alice) + receiver_balance = swap.balanceOf(bob) + + with boa.env.prank(loaded_alice): + swap.transfer(bob, amount) + + assert swap.balanceOf(loaded_alice) == 0 + assert swap.balanceOf(bob) == receiver_balance + amount + + +def test_transfer_zero_tokens(loaded_alice, bob, swap): + sender_balance = swap.balanceOf(loaded_alice) + receiver_balance = swap.balanceOf(bob) + + with boa.env.prank(loaded_alice): + swap.transfer(bob, 0) + + assert swap.balanceOf(loaded_alice) == sender_balance + assert swap.balanceOf(bob) == receiver_balance + + +def test_transfer_to_self(loaded_alice, swap): + sender_balance = swap.balanceOf(loaded_alice) + amount = sender_balance // 4 + + with boa.env.prank(loaded_alice): + swap.transfer(loaded_alice, amount) + + assert swap.balanceOf(loaded_alice) == sender_balance + + +def test_insufficient_balance(loaded_alice, bob, swap): + balance = swap.balanceOf(loaded_alice) + + with boa.reverts(), boa.env.prank(loaded_alice): + swap.transfer(bob, balance + 1) + + +def test_transfer_event_fires(loaded_alice, bob, swap): + amount = swap.balanceOf(loaded_alice) + with boa.env.prank(loaded_alice): + swap.transfer(bob, amount) + + logs = swap.get_logs() + + assert len(logs) == 1 + assert logs[0].event_type.name == "Transfer" + assert logs[0].args[0] == amount + assert logs[0].topics[0].lower() == loaded_alice.lower() + assert logs[0].topics[1].lower() == bob.lower() diff --git a/tests/unitary/pool/token/test_transferFrom.py b/tests/unitary/pool/token/test_transferFrom.py new file mode 100644 index 00000000..29060699 --- /dev/null +++ b/tests/unitary/pool/token/test_transferFrom.py @@ -0,0 +1,212 @@ +import boa + + +def test_sender_balance_decreases(loaded_alice, bob, charlie, swap): + sender_balance = swap.balanceOf(loaded_alice) + amount = sender_balance // 4 + + with boa.env.prank(loaded_alice): + swap.approve(bob, amount) + + with boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, amount) + + assert swap.balanceOf(loaded_alice) == sender_balance - amount + + +def test_receiver_balance_increases(loaded_alice, bob, charlie, swap): + receiver_balance = swap.balanceOf(charlie) + amount = swap.balanceOf(loaded_alice) // 4 + + with boa.env.prank(loaded_alice): + swap.approve(bob, amount) + + with boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, amount) + + assert swap.balanceOf(charlie) == receiver_balance + amount + + +def test_caller_balance_not_affected(loaded_alice, bob, charlie, swap): + caller_balance = swap.balanceOf(bob) + amount = swap.balanceOf(loaded_alice) + + with boa.env.prank(loaded_alice): + swap.approve(bob, amount) + + with boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, amount) + + assert swap.balanceOf(bob) == caller_balance + + +def test_caller_approval_affected(alice, bob, charlie, swap): + approval_amount = swap.balanceOf(alice) + transfer_amount = approval_amount // 4 + + with boa.env.prank(alice): + swap.approve(bob, approval_amount) + + with boa.env.prank(bob): + swap.transferFrom(alice, charlie, transfer_amount) + + assert swap.allowance(alice, bob) == approval_amount - transfer_amount + + +def test_receiver_approval_not_affected(loaded_alice, bob, charlie, swap): + approval_amount = swap.balanceOf(loaded_alice) + transfer_amount = approval_amount // 4 + + with boa.env.prank(loaded_alice): + swap.approve(bob, approval_amount) + swap.approve(charlie, approval_amount) + + with boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, transfer_amount) + + assert swap.allowance(loaded_alice, charlie) == approval_amount + + +def test_total_supply_not_affected(loaded_alice, bob, charlie, swap): + total_supply = swap.totalSupply() + amount = swap.balanceOf(loaded_alice) + + with boa.env.prank(loaded_alice): + swap.approve(bob, amount) + + with boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, amount) + + assert swap.totalSupply() == total_supply + + +def test_returns_true(loaded_alice, bob, charlie, swap): + amount = swap.balanceOf(loaded_alice) + with boa.env.prank(loaded_alice): + swap.approve(bob, amount) + + with boa.env.prank(bob): + assert swap.transferFrom(loaded_alice, charlie, amount) + + +def test_transfer_full_balance(loaded_alice, bob, charlie, swap): + amount = swap.balanceOf(loaded_alice) + receiver_balance = swap.balanceOf(charlie) + + with boa.env.prank(loaded_alice): + swap.approve(bob, amount) + + with boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, amount) + + assert swap.balanceOf(loaded_alice) == 0 + assert swap.balanceOf(charlie) == receiver_balance + amount + + +def test_transfer_zero_tokens(loaded_alice, bob, charlie, swap): + sender_balance = swap.balanceOf(loaded_alice) + receiver_balance = swap.balanceOf(charlie) + + with boa.env.prank(loaded_alice): + swap.approve(bob, sender_balance) + + with boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, 0) + + assert swap.balanceOf(loaded_alice) == sender_balance + assert swap.balanceOf(charlie) == receiver_balance + + +def test_transfer_zero_tokens_without_approval( + loaded_alice, bob, charlie, swap +): + sender_balance = swap.balanceOf(loaded_alice) + receiver_balance = swap.balanceOf(charlie) + + with boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, 0) + + assert swap.balanceOf(loaded_alice) == sender_balance + assert swap.balanceOf(charlie) == receiver_balance + + +def test_insufficient_balance(loaded_alice, bob, charlie, swap): + balance = swap.balanceOf(loaded_alice) + + with boa.env.prank(loaded_alice): + swap.approve(bob, balance + 1) + + with boa.reverts(), boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, balance + 1) + + +def test_insufficient_approval(loaded_alice, bob, charlie, swap): + balance = swap.balanceOf(loaded_alice) + + with boa.env.prank(loaded_alice): + swap.approve(bob, balance - 1) + + with boa.reverts(), boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, balance) + + +def test_no_approval(loaded_alice, bob, charlie, swap): + balance = swap.balanceOf(loaded_alice) + + with boa.reverts(), boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, balance) + + +def test_revoked_approval(loaded_alice, bob, charlie, swap): + balance = swap.balanceOf(loaded_alice) + + with boa.env.prank(loaded_alice): + swap.approve(bob, balance) + swap.approve(bob, 0) + + with boa.reverts(), boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, balance) + + +def test_transfer_to_self(loaded_alice, swap): + sender_balance = swap.balanceOf(loaded_alice) + amount = sender_balance // 4 + + with boa.env.prank(loaded_alice): + swap.approve(loaded_alice, sender_balance) + swap.transferFrom(loaded_alice, loaded_alice, amount) + + assert swap.balanceOf(loaded_alice) == sender_balance + assert ( + swap.allowance(loaded_alice, loaded_alice) == sender_balance - amount + ) + + +def test_transfer_to_self_no_approval(loaded_alice, swap): + amount = swap.balanceOf(loaded_alice) + + with boa.reverts(), boa.env.prank(loaded_alice): + swap.transferFrom(loaded_alice, loaded_alice, amount) + + +def test_transfer_event_fires(loaded_alice, bob, charlie, swap): + amount = swap.balanceOf(loaded_alice) + + with boa.env.prank(loaded_alice): + swap.approve(bob, amount) + + with boa.env.prank(bob): + swap.transferFrom(loaded_alice, charlie, amount) + + logs = swap.get_logs() + + assert len(logs) == 2 + assert logs[0].event_type.name == "Approval" + assert logs[0].args[0] == 0 # since everything got transferred + assert logs[0].topics[0].lower() == loaded_alice.lower() + assert logs[0].topics[1].lower() == bob.lower() + + assert logs[1].event_type.name == "Transfer" + assert logs[1].args[0] == amount + assert logs[1].topics[0].lower() == loaded_alice.lower() + assert logs[1].topics[1].lower() == charlie.lower() diff --git a/tests/utils/simulation_int_many.py b/tests/utils/simulation_int_many.py index d00d8149..d7d4d9e8 100644 --- a/tests/utils/simulation_int_many.py +++ b/tests/utils/simulation_int_many.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # flake8: noqa -import json +from math import exp from tests.unitary.math.misc import get_y_n2_dec @@ -20,7 +20,6 @@ def geometric_mean(x): diff = abs(D - D_prev) if diff <= 1 or diff * 10**18 < D: return D - print(x) raise ValueError("Did not converge") @@ -149,14 +148,11 @@ def solve_D(A, gamma, x): class Curve: - def __init__(self, A, gamma, D, n, p=None): + def __init__(self, A, gamma, D, n, p): self.A = A self.gamma = gamma self.n = n - if p: - self.p = p - else: - self.p = [10**18] * n + self.p = p self.x = [D // n * 10**18 // self.p[i] for i in range(n)] def xp(self): @@ -174,44 +170,26 @@ def y(self, x, i, j): yp = solve_x(self.A, self.gamma, xp, self.D(), j) return yp * 10**18 // self.p[j] + def get_p(self): -def get_data(fname): - with open("download/{0}-1m.json".format(fname), "r") as f: - return [ - { - "open": float(t[1]), - "high": float(t[2]), - "low": float(t[3]), - "close": float(t[4]), - "t": t[0] // 1000, - "volume": float(t[5]), - } - for t in json.load(f) - ] - - -def get_all(): - # 0 - usdt - # 1 - btc - # 2 - eth - out = [] - all_trades = { - name: get_data(name) for name in ["btcusdt", "ethusdt", "ethbtc"] - } - min_time = max(t[0]["t"] for t in all_trades.values()) - max_time = min(t[-1]["t"] for t in all_trades.values()) - for name, pair in [ - ("btcusdt", (0, 1)), - ("ethusdt", (0, 2)), - ("ethbtc", (1, 2)), - ]: - trades = all_trades[name] - for trade in trades: - if trade["t"] >= min_time and trade["t"] <= max_time: - trade["pair"] = pair - out.append((trade["t"] + sum(pair) * 15, trade)) - out = sorted(out) - return [i[1] for i in out] + A = self.A + gamma = self.gamma + xp = self.xp() + D = self.D() + + K0 = xp[0] * xp[1] * 4 // D * 10**36 // D + gK0 = ( + 2 * K0 * K0 // 10**36 * K0 // 10**36 + + (gamma + 10**18) ** 2 + - (K0 * K0 // 10**36 * (2 * gamma + 3 * 10**18) // 10**18) + ) + NNAG2 = A * gamma**2 // A_MULTIPLIER + numerator = ( + xp[0] * (gK0 + NNAG2 * xp[1] // D * K0 // 10**36) // xp[1] + ) + denominator = gK0 + NNAG2 * xp[0] // D * K0 // 10**36 + + return numerator * 10**18 // denominator class Trader: @@ -227,7 +205,7 @@ def __init__( allowed_extra_profit=2 * 10**13, fee_gamma=None, adjustment_step=0.003, - ma_half_time=500, + ma_time=866, log=True, ): # allowed_extra_profit is actually not used @@ -248,7 +226,7 @@ def __init__( self.log = log self.fee_gamma = fee_gamma or gamma self.total_vol = 0.0 - self.ma_half_time = ma_half_time + self.ma_time = ma_time self.ext_fee = 0 # 0.03e-2 self.slippage = 0 self.slippage_count = 0 @@ -257,28 +235,6 @@ def fee(self): f = reduction_coefficient(self.curve.xp(), self.fee_gamma) return (self.mid_fee * f + self.out_fee * (10**18 - f)) // 10**18 - def price(self, i, j): - dx_raw = self.dx * 10**18 // self.curve.p[i] - return ( - dx_raw - * 10**18 - // (self.curve.x[j] - self.curve.y(self.curve.x[i] + dx_raw, i, j)) - ) - - def step_for_price(self, dp, pair, sign=1): - a, b = pair - p0 = self.price(*pair) - dp = p0 * dp // 10**18 - x0 = self.curve.x[:] - step = self.dx * 10**18 // self.curve.p[a] - while True: - self.curve.x[a] = x0[a] + sign * step - dp_ = abs(p0 - self.price(*pair)) - if dp_ >= dp or step >= self.curve.x[a] // 10: - self.curve.x = x0 - return step - step *= 2 - def get_xcp(self): # First calculate the ideal balance # Then calculate, what the constant-product would be @@ -339,26 +295,31 @@ def sell(self, dy, i, j, min_price=0): except ValueError: return False + def _ma_multiplier(self, t): + return int(10**18 * exp(-1 * (t - self.t) / self.ma_time)) + def ma_recorder(self, t, price_vector): + # need to convert this to exp! # XXX what if every block only has p_b being last N = len(price_vector) if t > self.t: - alpha = 0.5 ** ((t - self.t) / self.ma_half_time) - for k in range(1, N): - self.price_oracle[k] = int( - price_vector[k] * (1 - alpha) - + self.price_oracle[k] * alpha + alpha = self._ma_multiplier(t) + last_price = min(price_vector[1], 2 * self.curve.p[1]) + self.price_oracle[1] = ( + int( + last_price * (10**18 - alpha) + + self.price_oracle[1] * alpha ) + // 10**18 + ) self.t = t - def tweak_price(self, t, a, b, p): + def tweak_price(self, t): + self.ma_recorder(t, self.last_price) - if b > 0: - self.last_price[b] = p * self.last_price[a] // 10**18 - else: - self.last_price[a] = self.last_price[0] * 10**18 // p + self.last_price[1] = self.curve.get_p() * self.curve.p[1] // 10**18 - # price_oracle looks like [1, p1, p2, ...] normalized to 1e18 + # update price_scale: norm = int( sum( (p_real * 10**18 // p_target - 10**18) ** 2 @@ -371,13 +332,10 @@ def tweak_price(self, t, a, b, p): # Already close to the target price return norm - p_new = [10**18] - p_new += [ - p_target + adjustment_step * (p_real - p_target) // norm - for p_real, p_target in zip( - self.price_oracle[1:], self.curve.p[1:] - ) - ] + price_scale_adjustment = ( + adjustment_step * (self.price_oracle[1] - self.curve.p[1]) // norm + ) + p_new = [10**18, self.curve.p[1] + price_scale_adjustment] old_p = self.curve.p[:] old_profit = self.xcp_profit_real @@ -393,153 +351,3 @@ def tweak_price(self, t, a, b, p): self.xcp = old_xcp return norm - - def simulate(self, mdata): - lasts = {} - self.t = mdata[0]["t"] - for i, d in enumerate(mdata): - a, b = d["pair"] - vol = 0 - ext_vol = int( - d["volume"] * self.price_oracle[b] - ) # <- now all is in USD - ctr = 0 - last = lasts.get( - (a, b), self.price_oracle[b] * 10**18 // self.price_oracle[a] - ) - _high = last - _low = last - - # Dynamic step - # f = reduction_coefficient(self.curve.xp(), self.curve.gamma) - candle = min( - int(1e18 * abs((d["high"] - d["low"]) / d["high"])), 10**17 - ) - candle = max(10**15, candle) - step1 = self.step_for_price(candle // 50, (a, b), sign=1) - step2 = self.step_for_price(candle // 50, (a, b), sign=-1) - step = min(step1, step2) - - max_price = int(1e18 * d["high"]) - _dx = 0 - p_before = self.price(a, b) - while last < max_price and vol < ext_vol // 2: - dy = self.buy(step, a, b, max_price=max_price) - if dy is False: - break - vol += dy * self.price_oracle[b] // 10**18 - _dx += dy - last = step * 10**18 // dy - max_price = int(1e18 * d["high"]) - ctr += 1 - p_after = self.price(a, b) - if p_before != p_after: - self.slippage_count += 1 - self.slippage += ( - _dx - * self.curve.p[b] - // 10**18 - * (p_before + p_after) - // (2 * abs(p_before - p_after)) - ) - _high = last - min_price = int(1e18 * d["low"]) - _dx = 0 - p_before = p_after - while last > min_price and vol < ext_vol // 2: - dx = step * 10**18 // last - dy = self.sell(dx, a, b, min_price=min_price) - _dx += dx - if dy is False: - break - vol += dx * self.price_oracle[b] // 10**18 - last = dy * 10**18 // dx - min_price = int(10**18 * d["low"]) - ctr += 1 - p_after = self.price(a, b) - if p_before != p_after: - self.slippage_count += 1 - self.slippage += ( - _dx - * self.curve.p[b] - // 10**18 - * (p_before + p_after) - // (2 * abs(p_before - p_after)) - ) - _low = last - lasts[(a, b)] = last - - self.tweak_price(d["t"], a, b, (_high + _low) // 2) - - self.total_vol += vol - if self.log: - try: - print( - ( - """{0:.1f}%\ttrades: {1}\t""" - """AMM: {2:.0f}, {3:.0f}\tTarget: {4:.0f}, {5:.0f}\t""" - """Vol: {6:.4f}\tPR:{7:.2f}\txCP-growth: {8:.5f}\t""" - """APY:{9:.1f}%\tfee:{10:.3f}%""" - ).format( - 100 * i / len(mdata), - ctr, - lasts.get( - (0, 1), - self.price_oracle[1] - * 10**18 - // self.price_oracle[0], - ) - / 1e18, - lasts.get( - (0, 2), - self.price_oracle[2] - * 10**18 - // self.price_oracle[0], - ) - / 1e18, - self.curve.p[1] / 1e18, - self.curve.p[2] / 1e18, - self.total_vol / 1e18, - (self.xcp_profit_real - 10**18) - / (self.xcp_profit - 10**18), - self.xcp_profit_real / 1e18, - ( - (self.xcp_profit_real / 1e18) - ** (86400 * 365 / (d["t"] - mdata[0]["t"] + 1)) - - 1 - ) - * 100, - self.fee() / 1e10 * 100, - ) - ) - except Exception: - pass - - -def get_price_vector(n, data): - p = [10**18] + [None] * (n - 1) - for d in data: - if d["pair"][0] == 0: - p[d["pair"][1]] = int(d["close"] * 1e18) - if all(x is not None for x in p): - return p - - -if __name__ == "__main__": - test_data = get_all()[-100000:] - - trader = Trader( - 135 * 3**3 * 10000, - int(7e-5 * 1e18), - 5_000_000 * 10**18, - 3, - get_price_vector(3, test_data), - mid_fee=4e-4, - out_fee=4.0e-3, - allowed_extra_profit=2 * 10**13, - fee_gamma=int(0.01 * 1e18), - adjustment_step=0.0015, - ma_half_time=600, - ) - - trader.simulate(test_data) From 50b381bbaf7467f5669866700f75a19a8734ddad Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Sat, 23 Dec 2023 14:12:21 +0530 Subject: [PATCH 71/72] fix: lp price isqrt input --- contracts/main/CurveTwocryptoOptimized.vy | 2 +- tests/unitary/pool/test_oracles.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/main/CurveTwocryptoOptimized.vy b/contracts/main/CurveTwocryptoOptimized.vy index 38195ed3..d036988d 100644 --- a/contracts/main/CurveTwocryptoOptimized.vy +++ b/contracts/main/CurveTwocryptoOptimized.vy @@ -1630,7 +1630,7 @@ def lp_price() -> uint256: 0th index @return uint256 LP price. """ - return 2 * self.virtual_price * isqrt(self.internal_price_oracle()) / 10**18 + return 2 * self.virtual_price * isqrt(self.internal_price_oracle() * 10**18) / 10**18 @external diff --git a/tests/unitary/pool/test_oracles.py b/tests/unitary/pool/test_oracles.py index c3808777..96cf2b16 100644 --- a/tests/unitary/pool/test_oracles.py +++ b/tests/unitary/pool/test_oracles.py @@ -219,4 +219,4 @@ def test_lp_price(swap_with_deposit): // 10**18 ) naive_price = tvl * 10**18 // swap_with_deposit.totalSupply() - assert abs(swap_with_deposit.lp_price() / naive_price - 1) < 2e-3 + assert approx(naive_price, swap_with_deposit.lp_price(), 0) From b8601aeab35d140b5bcefc866cfec80514c6bc51 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Sat, 23 Dec 2023 15:17:46 +0530 Subject: [PATCH 72/72] fix: get_p test --- tests/unitary/math/test_get_p.py | 43 +++++++++----------- tests/unitary/pool/stateful/test_simulate.py | 23 +++-------- tests/unitary/pool/test_deposit_withdraw.py | 7 +--- tests/unitary/pool/test_oracles.py | 7 +--- tests/utils/__init__.py | 5 +++ 5 files changed, 32 insertions(+), 53 deletions(-) diff --git a/tests/unitary/math/test_get_p.py b/tests/unitary/math/test_get_p.py index 3ebf6bfb..7d0eae5f 100644 --- a/tests/unitary/math/test_get_p.py +++ b/tests/unitary/math/test_get_p.py @@ -1,14 +1,13 @@ -from math import log - import boa import pytest from boa.test import strategy from hypothesis import given, settings from tests.fixtures.pool import INITIAL_PRICES +from tests.utils import approx from tests.utils.tokens import mint_for_testing -SETTINGS = {"max_examples": 1000, "deadline": None} +SETTINGS = {"max_examples": 20, "deadline": None} # flake8: noqa: E501 @@ -59,18 +58,18 @@ def _get_prices_vyper(swap, price_calc): def _get_prices_numeric_nofee(swap, views, i): - if i == 0: # we are selling j - - dx = 10**16 # 0.01 USD - dy = views.internal._get_dy_nofee(0, 1, dx, swap)[0] - price = dx * 10**18 // dy[0] - - else: # we are buying j so numba should go up + if i == 0: # token at index 1 is being pupmed. dx = int(0.01 * 10**36 // INITIAL_PRICES[1]) dolla_out = views.internal._get_dy_nofee(1, 0, dx, swap)[0] price = dolla_out * 10**18 // dx + else: # token at index 1 is being dupmed. + + dx = 10**16 # 0.01 USD + dy = views.internal._get_dy_nofee(0, 1, dx, swap)[0] + price = dx * 10**18 // dy + return price @@ -78,9 +77,7 @@ def _get_prices_numeric_nofee(swap, views, i): @given( - dollar_amount=strategy( - "decimal", min_value=10**-5, max_value=5 * 10**8 - ), + dollar_amount=strategy("decimal", min_value=1e-5, max_value=5e8), ) @settings(**SETTINGS) @pytest.mark.parametrize("i", [0, 1]) @@ -97,22 +94,20 @@ def test_dxdy_similar( previous_p = yuge_swap.price_scale() j = 1 - i - dx = int(dollar_amount * 10**36 // INITIAL_PRICES[i]) - mint_for_testing(coins[i], user, dx) - out = yuge_swap.exchange(i, j, dx, 0, sender=user) + amount_in = int(dollar_amount * 10**36 // INITIAL_PRICES[i]) + mint_for_testing(coins[i], user, amount_in) + yuge_swap.exchange(i, j, amount_in, 0, sender=user) dxdy_vyper = _get_prices_vyper(yuge_swap, dydx_safemath) dxdy_swap = yuge_swap.last_prices() # <-- we check unsafe impl here. - dxdy_numeric_nofee = _get_prices_numeric_nofee( - yuge_swap, views_contract, i - ) + dxdy_numeric = _get_prices_numeric_nofee(yuge_swap, views_contract, i) - if i == 0: # j is being pupmed + if i == 0: # token at index 1 is being pupmed, so last_price should go up assert dxdy_swap > previous_p - assert dxdy_numeric_nofee > previous_p - else: # j is being dupmed + assert dxdy_numeric > previous_p + else: # token at index 1 is being dupmed, so last_price should go down assert dxdy_swap < previous_p - assert dxdy_numeric_nofee < previous_p + assert dxdy_numeric < previous_p assert dxdy_vyper == dxdy_swap - assert abs(log(dxdy_vyper / dxdy_numeric_nofee)) < 1e-5 + assert approx(dxdy_vyper, dxdy_numeric, 1e-5) diff --git a/tests/unitary/pool/stateful/test_simulate.py b/tests/unitary/pool/stateful/test_simulate.py index 53dc3876..3b86f37b 100644 --- a/tests/unitary/pool/stateful/test_simulate.py +++ b/tests/unitary/pool/stateful/test_simulate.py @@ -1,10 +1,9 @@ -from math import log - import boa from boa.test import strategy from hypothesis.stateful import invariant, rule, run_state_machine_as_test from tests.unitary.pool.stateful.stateful_base import StatefulBase +from tests.utils import approx from tests.utils import simulation_int_many as sim from tests.utils.tokens import mint_for_testing @@ -12,14 +11,6 @@ STEP_COUNT = 100 -def approx(x1, x2, precision): - return abs(log(x1 / x2)) <= precision - - -def logdiff(x1, x2): - return abs(log(x1 / x2)) - - class StatefulSimulation(StatefulBase): exchange_amount_in = strategy( "uint256", min_value=10**17, max_value=10**5 * 10**18 @@ -87,14 +78,11 @@ def exchange(self, exchange_amount_in, exchange_i, user): self.trader.tweak_price(boa.env.vm.state.timestamp) # exchange checks: - out_logdiff = logdiff(self.swap_out, dy_trader) - price_oracle_logdiff = logdiff( - self.swap.price_oracle(), self.trader.price_oracle[1] + assert approx(self.swap_out, dy_trader, 1e-3) + assert approx( + self.swap.price_oracle(), self.trader.price_oracle[1], 1e-3 ) - assert out_logdiff <= 1e-3 - assert price_oracle_logdiff <= 1e-3 - boa.env.time_travel(12) @invariant() @@ -108,9 +96,8 @@ def simulator(self): price_scale = self.swap.price_scale() price_trader = self.trader.curve.p[1] - price_scale_logdiff = logdiff(price_scale, price_trader) try: - assert price_scale_logdiff <= 1e-3 + assert approx(price_scale, price_trader, 1e-3) except Exception: if self.check_limits([0, 0, 0]): assert False diff --git a/tests/unitary/pool/test_deposit_withdraw.py b/tests/unitary/pool/test_deposit_withdraw.py index 687c02e7..b70bffee 100644 --- a/tests/unitary/pool/test_deposit_withdraw.py +++ b/tests/unitary/pool/test_deposit_withdraw.py @@ -1,21 +1,16 @@ -import math - import boa import pytest from boa.test import strategy from hypothesis import given, settings from tests.fixtures.pool import INITIAL_PRICES +from tests.utils import approx from tests.utils import simulation_int_many as sim from tests.utils.tokens import mint_for_testing SETTINGS = {"max_examples": 100, "deadline": None} -def approx(x1, x2, precision): - return abs(math.log(x1 / x2)) <= precision - - def assert_string_contains(string, substrings): assert any(substring in string for substring in substrings) diff --git a/tests/unitary/pool/test_oracles.py b/tests/unitary/pool/test_oracles.py index 96cf2b16..80666a0c 100644 --- a/tests/unitary/pool/test_oracles.py +++ b/tests/unitary/pool/test_oracles.py @@ -1,4 +1,4 @@ -from math import exp, log, log2, sqrt +from math import exp, log2, sqrt import boa import pytest @@ -6,15 +6,12 @@ from hypothesis import given, settings from tests.fixtures.pool import INITIAL_PRICES +from tests.utils import approx from tests.utils.tokens import mint_for_testing SETTINGS = {"max_examples": 1000, "deadline": None} -def approx(x1, x2, precision): - return abs(log(x1 / x2)) <= precision - - def norm(price_oracle, price_scale): norm = 0 ratio = price_oracle * 10**18 / price_scale diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index e69de29b..c10431db 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -0,0 +1,5 @@ +import math + + +def approx(x1, x2, precision): + return abs(math.log(x1 / x2)) <= precision