diff --git a/contracts/experimental/initial_guess/CurveCryptoMathOptimized2.vy b/contracts/experimental/initial_guess/CurveCryptoMathOptimized2.vy index 20f7b2b2..4bc0a644 100644 --- a/contracts/experimental/initial_guess/CurveCryptoMathOptimized2.vy +++ b/contracts/experimental/initial_guess/CurveCryptoMathOptimized2.vy @@ -19,7 +19,7 @@ A_MULTIPLIER: constant(uint256) = 10000 MIN_GAMMA: constant(uint256) = 10**10 MAX_GAMMA_SMALL: constant(uint256) = 2 * 10**16 -MAX_GAMMA: constant(uint256) = 3 * 10**17 +MAX_GAMMA: constant(uint256) = 199 * 10**15 # 1.99 * 10**17 MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 @@ -373,7 +373,7 @@ def get_y( @external @view -def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], initial_D: uint256 = 0) -> uint256: +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 @@ -395,12 +395,13 @@ def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], initial S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds D: uint256 = 0 - if initial_D == 0: - D = N_COINS * isqrt(unsafe_mul(x[0], x[1])) # Naive initial guess + if K0_prev == 0: + D = N_COINS * isqrt(unsafe_mul(x[0], x[1])) else: - D = initial_D + # D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18) + D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18)) if S < D: - D = S # TODO: Check this! + D = S __g1k0: uint256 = gamma + 10**18 diff: uint256 = 0 diff --git a/contracts/experimental/initial_guess/CurveTwocryptoOptimized.vy b/contracts/experimental/initial_guess/CurveTwocryptoOptimized.vy index 54c01a2c..bdcf8680 100644 --- a/contracts/experimental/initial_guess/CurveTwocryptoOptimized.vy +++ b/contracts/experimental/initial_guess/CurveTwocryptoOptimized.vy @@ -100,7 +100,6 @@ event NewParameters: allowed_extra_profit: uint256 adjustment_step: uint256 ma_time: uint256 - xcp_ma_time: uint256 event RampAgamma: initial_A: uint256 @@ -132,12 +131,9 @@ factory: public(immutable(Factory)) 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: public(uint256) last_timestamp: public(uint256) # idx 0 is for prices, idx 1 is for xcp. -last_xcp: public(uint256) -xcp_ma_time: public(uint256) initial_A_gamma: public(uint256) initial_A_gamma_time: public(uint256) @@ -183,7 +179,7 @@ MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 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) = 3 * 10**17 +MAX_GAMMA: constant(uint256) = 199 * 10**15 # 1.99 * 10**17 # ----------------------- ERC20 Specific vars -------------------------------- @@ -258,9 +254,8 @@ def __init__( self.cached_price_scale = initial_price self.cached_price_oracle = initial_price self.last_prices = initial_price - self.last_timestamp = self._pack_2(block.timestamp, block.timestamp) + self.last_timestamp = block.timestamp self.xcp_profit_a = 10**18 - self.xcp_ma_time = 62324 # <--------- 12 hours default on contract start. # Cache DOMAIN_SEPARATOR. If chain.id is not CACHED_CHAIN_ID, then # DOMAIN_SEPARATOR will be re-calculated each time `permit` is called. @@ -526,14 +521,13 @@ def add_liquidity( old_D = self.D - D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)# TODO: can use use old_D here? + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) token_supply: uint256 = self.totalSupply if old_D > 0: d_token = token_supply * D / old_D - token_supply else: - # Make initial virtual price equal to 1: - d_token = self.get_xcp(D, price_scale) + d_token = self.get_xcp(D, price_scale) # <----- Making initial virtual price equal to 1. assert d_token > 0 # dev: nothing minted @@ -546,10 +540,7 @@ def add_liquidity( d_token -= d_token_fee token_supply += d_token self.mint(receiver, d_token) - self.admin_lp_virtual_balance += unsafe_div( - ADMIN_FEE * d_token_fee, - 10**10 - ) + self.admin_lp_virtual_balance += unsafe_div(ADMIN_FEE * d_token_fee, 10**10) price_scale = self.tweak_price(A_gamma, xp, D, 0) @@ -562,9 +553,6 @@ def add_liquidity( self.xcp_profit = 10**18 self.xcp_profit_a = 10**18 - # Initialise xcp oracle here as virtual_price * totalSupply / 10**18: - self.cached_xcp_oracle = d_token - self.mint(receiver, d_token) assert d_token >= min_mint_amount, "Slippage" @@ -645,38 +633,6 @@ 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) - 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: - - cached_xcp_oracle: uint256 = self.cached_xcp_oracle - alpha: uint256 = MATH.wad_exp( - -convert( - unsafe_div( - unsafe_sub(block.timestamp, last_timestamp[1]) * 10**18, - self.xcp_ma_time # <---------- xcp ma time has is longer. - ), - int256, - ) - ) - - self.cached_xcp_oracle = unsafe_div( - last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, - 10**18 - ) - last_timestamp[1] = block.timestamp - - # 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 @@ -725,12 +681,9 @@ def remove_liquidity_one_coin( # Burn user's tokens: self.burnFrom(msg.sender, token_amount) - packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0) # NOTE: Here we don't need an initial - # Safe to use D from _calc_withdraw_one_coin here ---^ guess since new_D has already - # been calculated, and will be - # used as an initial guess should - # the algorithm decide to rebalance - # position. + packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0) + # Safe to use D from _calc_withdraw_one_coin here ---^ + # ------------------------- Transfers ------------------------------------ # _transfer_out updates self.balances here. Update to state occurs before @@ -824,7 +777,7 @@ def _exchange( x1: uint256 = xp[i] # <------------------ Back up old value in xp ... xp[i] = x0 # | - self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # | TODO: can we use self.D here? + self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # | xp[i] = x1 # <-------------------------------------- ... and restore. # ----------------------- Calculate dy and fees -------------------------- @@ -852,12 +805,7 @@ def _exchange( # ------ Tweak price_scale with good initial guess for newton_D ---------- # Get initial guess using: D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18) - initial_D: uint256 = isqrt( - unsafe_mul( - unsafe_div(unsafe_mul(unsafe_mul(4, xp[0]), xp[1]), y_out[1]), - 10**18 - ) - ) + initial_D: uint256 = isqrt(xp[0] * xp[1] * 4 / y_out[1] * 10**18) price_scale = self.tweak_price(A_gamma, xp, 0, initial_D) return [dy, fee, price_scale] @@ -879,7 +827,7 @@ def tweak_price( @param A_gamma Array of A and gamma parameters. @param _xp Array of current balances. @param new_D New D value. - @param initial_D Initial guess of D value for `newton_D`. + @param initial_D Initial guess for `newton_D`. """ # ---------------------------- Read storage ------------------------------ @@ -894,11 +842,11 @@ def tweak_price( old_xcp_profit: uint256 = self.xcp_profit old_virtual_price: uint256 = self.virtual_price - # ----------------------- Update Oracles if needed ----------------------- + # ------------------ Update Price Oracle if needed ----------------------- - last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) + last_timestamp: uint256 = self.last_timestamp alpha: uint256 = 0 - if last_timestamp[0] < block.timestamp: # 0th index is for price_oracle. + if last_timestamp < 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 @@ -909,7 +857,7 @@ def tweak_price( alpha = MATH.wad_exp( -convert( unsafe_div( - unsafe_sub(block.timestamp, last_timestamp[0]) * 10**18, + unsafe_sub(block.timestamp, last_timestamp) * 10**18, rebalancing_params[2] # <----------------------- ma_time. ), int256, @@ -927,32 +875,7 @@ def tweak_price( ) self.cached_price_oracle = price_oracle - last_timestamp[0] = block.timestamp - - # ----------------------------------------------------- Update xcp oracle. - - if last_timestamp[1] < block.timestamp: - - cached_xcp_oracle: uint256 = self.cached_xcp_oracle - alpha = MATH.wad_exp( - -convert( - unsafe_div( - unsafe_sub(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 - ) - - # Pack and store timestamps: - last_timestamp[1] = block.timestamp - - self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) + self.last_timestamp = block.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 @@ -996,11 +919,8 @@ def tweak_price( # ensure new virtual_price is not less than old virtual_price, # else the pool suffers a loss. if self.future_A_gamma_time < block.timestamp: - assert virtual_price > old_virtual_price, "Loss" - - # -------------------------- Cache last_xcp -------------------------- - - self.last_xcp = xcp # geometric_mean(D * price_scale) + # this usually reverts when withdrawing a very small amount of LP tokens + assert virtual_price > old_virtual_price # dev: virtual price decreased self.xcp_profit = xcp_profit @@ -1045,14 +965,12 @@ def tweak_price( ] # ------------------------------------------ Update D with new xp. - # NOTE: We are also using D_unadjusted - # here as an initial guess! D: uint256 = MATH.newton_D( - A_gamma[0], - A_gamma[1], - xp, - D_unadjusted, # <-------------------------------------------- NOTE: Previously we did not use any - ) # initial guesses. + A_gamma[0], + A_gamma[1], + xp, + D_unadjusted + ) for k in range(N_COINS): frac: uint256 = xp[k] * 10**18 / D # <----- Check validity of @@ -1363,7 +1281,7 @@ def _calc_withdraw_one_coin( # --------- Calculate `approx_fee` (assuming balanced state) in ith token. # -------------------------------- We only need this for fee in the event. - approx_fee: uint256 = N_COINS * D_fee * xx[i] / D + approx_fee: uint256 = N_COINS * D_fee * xx[i] / D # <------------------<---------- TODO: Check math. # ------------------------------------------------------------------------ D -= (dD - D_fee) # <----------------------------------- Charge fee on D. @@ -1552,7 +1470,7 @@ def internal_price_oracle() -> uint256: """ price_oracle: uint256 = self.cached_price_oracle price_scale: uint256 = self.cached_price_scale - last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[0] + last_prices_timestamp: uint256 = self.last_timestamp if last_prices_timestamp < block.timestamp: # <------------ Update moving # average if needed. @@ -1683,41 +1601,6 @@ def price_oracle() -> uint256: return self.internal_price_oracle() -@external -@view -@nonreentrant("lock") -def xcp_oracle() -> uint256: - """ - @notice Returns the oracle value for xcp. - @dev The oracle is an exponential moving average, with a periodicity - determined by `self.xcp_ma_time`. - `TVL` is xcp, calculated as either: - 1. virtual_price * total_supply, OR - 2. self.get_xcp(...), OR - 3. MATH.geometric_mean(xp) - @return uint256 Oracle value of xcp. - """ - - 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: - - alpha: uint256 = MATH.wad_exp( - -convert( - unsafe_div( - unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18, - self.xcp_ma_time - ), - int256, - ) - ) - - return (self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha) / 10**18 - - return cached_xcp_oracle - - @external @view @nonreentrant("lock") @@ -1905,7 +1788,7 @@ def ramp_A_gamma( @param future_time The timestamp at which the ramping will end. """ assert msg.sender == factory.admin() # dev: only owner - assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing + assert block.timestamp > self.future_A_gamma_time # dev: ramp undergoing assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time A_gamma: uint256[2] = self._A_gamma() @@ -1918,12 +1801,12 @@ def ramp_A_gamma( assert future_gamma < MAX_GAMMA + 1 ratio: uint256 = 10**18 * future_A / A_gamma[0] - assert ratio < 10**18 * MAX_A_CHANGE + 1 - assert ratio > 10**18 / MAX_A_CHANGE - 1 + assert ratio < 10**18 * MAX_A_CHANGE + 1 # dev: A change too high + assert ratio > 10**18 / MAX_A_CHANGE - 1 # dev: A change too low ratio = 10**18 * future_gamma / A_gamma[1] - assert ratio < 10**18 * MAX_A_CHANGE + 1 - assert ratio > 10**18 / MAX_A_CHANGE - 1 + assert ratio < 10**18 * MAX_A_CHANGE + 1 # dev: gamma change too high + assert ratio > 10**18 / MAX_A_CHANGE - 1 # dev: gamma change too low self.initial_A_gamma = initial_A_gamma self.initial_A_gamma_time = block.timestamp @@ -1973,7 +1856,6 @@ def apply_new_parameters( _new_allowed_extra_profit: uint256, _new_adjustment_step: uint256, _new_ma_time: uint256, - _new_xcp_ma_time: uint256, ): """ @notice Commit new parameters. @@ -1984,7 +1866,6 @@ def apply_new_parameters( @param _new_allowed_extra_profit The new allowed extra profit. @param _new_adjustment_step The new adjustment step. @param _new_ma_time The new ma time. ma_time is time_in_seconds/ln(2). - @param _new_xcp_ma_time The new ma time for xcp oracle. """ assert msg.sender == factory.admin() # dev: only owner @@ -2035,14 +1916,6 @@ def apply_new_parameters( [new_allowed_extra_profit, new_adjustment_step, new_ma_time] ) - # Set xcp oracle moving average window time: - new_xcp_ma_time: uint256 = _new_xcp_ma_time - if new_xcp_ma_time < 872542: - assert new_xcp_ma_time > 86 # dev: xcp MA time should be longer than 60/ln(2) - else: - new_xcp_ma_time = self.xcp_ma_time - self.xcp_ma_time = new_xcp_ma_time - # ---------------------------------- LOG --------------------------------- log NewParameters( @@ -2052,5 +1925,4 @@ def apply_new_parameters( new_allowed_extra_profit, new_adjustment_step, new_ma_time, - _new_xcp_ma_time, ) diff --git a/tests/profiling/conftest.py b/tests/profiling/conftest.py new file mode 100644 index 00000000..33062bb8 --- /dev/null +++ b/tests/profiling/conftest.py @@ -0,0 +1,167 @@ +import boa +import pytest + +from hypothesis import assume + +# compiling contracts +from contracts.main import CurveCryptoViews2Optimized as view_deployer +from contracts.main import CurveTwocryptoFactory as factory_deployer + +from contracts.main import CurveTwocryptoOptimized as amm_deployer +from contracts.main import CurveCryptoMathOptimized2 as math_deployer +from contracts.experimental.initial_guess import CurveTwocryptoOptimized as amm_deployer_initial_guess +from contracts.experimental.initial_guess import CurveCryptoMathOptimized2 as math_deployer_initial_guess +from tests.utils.tokens import mint_for_testing + +# ---------------- addresses ---------------- +address = boa.test.strategy("address") +deployer = address +fee_receiver = address +owner = address +params = { + "A": 400000, + "gamma": 145000000000000, + "mid_fee": 26000000, + "out_fee": 45000000, + "allowed_extra_profit": 2000000000000, + "fee_gamma": 230000000000000, + "adjustment_step": 146000000000000, + "ma_exp_time": 866, # # 600 seconds//math.log(2) + "price": 4000 * 10**18, +} + + +def _deposit_initial_liquidity(pool, tokens): + + # deposit: + user = boa.env.generate_address() + quantities = [10**6 * 10**36 // p for p in [10**18, params["price"]]] # $2M worth + + for coin, quantity in zip(tokens, quantities): + # mint coins for user: + mint_for_testing(coin, user, quantity) + assert coin.balanceOf(user) == quantity + + # approve crypto_swap to trade coin for user: + with boa.env.prank(user): + coin.approve(pool, 2**256 - 1) + + # Very first deposit + with boa.env.prank(user): + pool.add_liquidity(quantities, 0) + + return pool + + +@pytest.fixture(scope="module") +def tokens(): + return [ + boa.load("contracts/mocks/ERC20Mock.vy", "tkn_a", "tkn_a", 18), + boa.load("contracts/mocks/ERC20Mock.vy", "tkn_b", "tkn_b", 18) +] + + +@pytest.fixture(scope="module") +def factory_no_initial_guess(): + + _deployer = boa.env.generate_address() + _fee_receiver = boa.env.generate_address() + _owner = boa.env.generate_address() + + with boa.env.prank(_deployer): + + amm_implementation = amm_deployer.deploy_as_blueprint() + math_contract = math_deployer.deploy() + view_contract = view_deployer.deploy() + + _factory = factory_deployer.deploy() + _factory.initialise_ownership(_fee_receiver, _owner) + + with boa.env.prank(_owner): + _factory.set_views_implementation(view_contract) + _factory.set_math_implementation(math_contract) + + # set pool implementations: + _factory.set_pool_implementation(amm_implementation, 0) + + return _factory + + +@pytest.fixture(scope="module") +def factory_initial_guess(): + + _deployer = boa.env.generate_address() + _fee_receiver = boa.env.generate_address() + _owner = boa.env.generate_address() + + assume(_fee_receiver != _owner != _deployer) + + with boa.env.prank(_deployer): + amm_implementation = amm_deployer_initial_guess.deploy_as_blueprint() + math_contract = math_deployer_initial_guess.deploy() + view_contract = view_deployer.deploy() + + _factory = factory_deployer.deploy() + _factory.initialise_ownership(_fee_receiver, _owner) + + with boa.env.prank(_owner): + _factory.set_views_implementation(view_contract) + _factory.set_math_implementation(math_contract) + + # set pool implementations: + _factory.set_pool_implementation(amm_implementation, 0) + + return _factory + + +@pytest.fixture(scope="module") +def pool(factory, tokens): + + with boa.env.prank(boa.env.generate_address()): + _pool = factory.deploy_pool( + "test_A", + "test_A", + tokens, + 0, + params["A"], + params["gamma"], + params["mid_fee"], + params["out_fee"], + params["fee_gamma"], + params["allowed_extra_profit"], + params["adjustment_step"], + params["ma_exp_time"], + params["price"], + ) + + _pool = amm_deployer.at(_pool) + return _deposit_initial_liquidity(_pool, tokens) + + +@pytest.fixture(scope="module") +def pool_initial_guess(factory_initial_guess, tokens): + + with boa.env.prank(boa.env.generate_address()): + _pool = factory_initial_guess.deploy_pool( + "test_B", + "test_B", + tokens, + 0, + params["A"], + params["gamma"], + params["mid_fee"], + params["out_fee"], + params["fee_gamma"], + params["allowed_extra_profit"], + params["adjustment_step"], + params["ma_exp_time"], + params["price"], + ) + + _pool = amm_deployer.at(_pool) + return _deposit_initial_liquidity(_pool, tokens) + + +@pytest.fixture(scope="module") +def pools(pool, pool_initial_guess): + return [pool, pool_initial_guess] diff --git a/tests/profiling/test_boa_profile.py b/tests/profiling/test_boa_profile.py new file mode 100644 index 00000000..5fd19d80 --- /dev/null +++ b/tests/profiling/test_boa_profile.py @@ -0,0 +1,66 @@ +import random + +import boa +import pytest + +from tests.utils.tokens import mint_for_testing + + +NUM_RUNS = 10 +N_COINS = 2 + + +def _choose_indices(): + i = random.randint(0, N_COINS-1) + j = 0 if i == 1 else 1 + return i, j + + +@pytest.mark.profile +def test_profile_amms(pools, tokens): + + user = boa.env.generate_address() + + for pool in pools: + + for coin in tokens: + mint_for_testing(coin, user, 10**50) + coin.approve(pool, 2**256 - 1, sender=user) + + with boa.env.prank(user): + + for k in range(NUM_RUNS): + + # proportional deposit: + balances = [pool.balances(i) for i in range(N_COINS)] + c = random.uniform(0, 0.05) + amounts = [int(c * i * random.uniform(0, 0.8)) for i in balances] + pool.add_liquidity(amounts, 0) + boa.env.time_travel(random.randint(12, 600)) + + # deposit single token: + balances = [pool.balances(i) for i in range(N_COINS)] + c = random.uniform(0, 0.05) + i = random.randint(0, N_COINS-1) + amounts = [0] * N_COINS + for j in range(N_COINS): + if i == j: + amounts[i] = int(balances[i] * c) + pool.add_liquidity(amounts, 0) + boa.env.time_travel(random.randint(12, 600)) + + # swap: + i, j = _choose_indices() + amount = int(pool.balances(i) * 0.01) + pool.exchange(i, j, amount, 0) + boa.env.time_travel(random.randint(12, 600)) + + # withdraw proportionally: + amount = int(pool.balanceOf(user) * random.uniform(0, 0.01)) + pool.remove_liquidity(amount, [0] * N_COINS) + boa.env.time_travel(random.randint(12, 600)) + + # withdraw in one coin: + i = random.randint(0, N_COINS-1) + amount = int(pool.balanceOf(user) * 0.01) + pool.remove_liquidity_one_coin(amount, i, 0)