diff --git a/README.md b/README.md index eb72e7fe..2d2bed53 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ Ethereum: 5. Gauge Blueprint: [0x5fC124a161d888893529f67580ef94C2784e9233](https://etherscan.io/address/0x5fC124a161d888893529f67580ef94C2784e9233) 6. TricryptoFactoryHandler: [0x30a4249C42be05215b6063691949710592859697](https://etherscan.io/address/0x30a4249C42be05215b6063691949710592859697) +Updated AMM Blueprint (14-09-2023): [0xbC0797015fcFc47d9C1856639CaE50D0e69FbEE8](https://etherscan.io/address/0xbC0797015fcFc47d9C1856639CaE50D0e69FbEE8) + Deployed Pool: 1. [TricryptoUSDC 0x7f86bf177dd4f3494b841a37e810a34dd56c829b](https://etherscan.io/address/0x7f86bf177dd4f3494b841a37e810a34dd56c829b) @@ -64,6 +66,8 @@ Arbitrum: 3. Math: [0x604388Bb1159AFd21eB5191cE22b4DeCdEE2Ae22](https://arbiscan.io/address/0x604388Bb1159AFd21eB5191cE22b4DeCdEE2Ae22) 4. Views: [0x06452f9c013fc37169B57Eab8F50A7A48c9198A3](https://arbiscan.io/address/0x06452f9c013fc37169B57Eab8F50A7A48c9198A3) +Updated AMM Blueprint (14-09-2023): [0xbC0797015fcFc47d9C1856639CaE50D0e69FbEE8](https://arbiscan.io/address/0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC) + Deployed Pool: 1. [TricryptoUSDC 0x7706128aFAC8875981b2412faC6C4f3053EA705f](https://arbiscan.io/address/0x7706128aFAC8875981b2412faC6C4f3053EA705f) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index b29c68b6..55154838 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -3,8 +3,8 @@ """ @title CurveTricryptoOptimizedWETH @author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -@notice A Curve AMM pool for 3 unpegged assets (e.g. ETH, BTC, USD). +@license Copyright (c) Curve.Fi, 2023 - all rights reserved +@notice A Curve AMM pool for 3 unpegged assets (e.g. WETH, BTC, USD). @dev All prices in the AMM are with respect to the first token in the pool. """ @@ -37,10 +37,6 @@ interface Math: _xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[2], ) -> uint256[N_COINS-1]: view -interface WETH: - def deposit(): payable - def withdraw(_amount: uint256): nonpayable - interface Factory: def admin() -> address: view def fee_receiver() -> address: view @@ -99,15 +95,6 @@ event RemoveLiquidityOne: approx_fee: uint256 packed_price_scale: uint256 -event CommitNewParameters: - deadline: indexed(uint256) - mid_fee: uint256 - out_fee: uint256 - fee_gamma: uint256 - allowed_extra_profit: uint256 - adjustment_step: uint256 - ma_time: uint256 - event NewParameters: mid_fee: uint256 out_fee: uint256 @@ -115,6 +102,7 @@ event NewParameters: allowed_extra_profit: uint256 adjustment_step: uint256 ma_time: uint256 + xcp_ma_time: uint256 event RampAgamma: initial_A: uint256 @@ -131,7 +119,7 @@ event StopRampA: event ClaimAdminFee: admin: indexed(address) - tokens: uint256 + tokens: uint256[N_COINS] # ----------------------- Storage/State Variables ---------------------------- @@ -141,17 +129,20 @@ WETH20: public(immutable(address)) N_COINS: constant(uint256) = 3 PRECISION: constant(uint256) = 10**18 # <------- The precision to convert to. A_MULTIPLIER: constant(uint256) = 10000 -packed_precisions: uint256 +PRECISIONS: immutable(uint256[N_COINS]) MATH: public(immutable(Math)) coins: public(immutable(address[N_COINS])) -factory: public(address) +factory: public(immutable(Factory)) price_scale_packed: uint256 # <------------------------ Internal price scale. price_oracle_packed: uint256 # <------- Price target given by moving average. +cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. last_prices_packed: uint256 last_prices_timestamp: public(uint256) +last_xcp: public(uint256) +xcp_ma_time: public(uint256) initial_A_gamma: public(uint256) initial_A_gamma_time: public(uint256) @@ -172,17 +163,12 @@ xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees. virtual_price: public(uint256) # <------ Cached (fast to read) virtual price. # The cached `virtual_price` is also used internally. -# -------------- Params that affect how price_scale get adjusted ------------- - +# Params that affect how price_scale get adjusted : packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing # parameters allowed_extra_profit, adjustment_step, and ma_time. -future_packed_rebalancing_params: uint256 - -# ---------------- Fee params that determine dynamic fees -------------------- - +# Fee params that determine dynamic fees: packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma. -future_packed_fee_params: uint256 ADMIN_FEE: public(constant(uint256)) = 5 * 10**9 # <----- 50% of earned fees. MIN_FEE: constant(uint256) = 5 * 10**5 # <-------------------------- 0.5 BPS. @@ -191,10 +177,10 @@ NOISE_FEE: constant(uint256) = 10**5 # <---------------------------- 0.1 BPS. # ----------------------- Admin params --------------------------------------- -admin_actions_deadline: public(uint256) +last_admin_fee_claim_timestamp: uint256 -ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 MIN_RAMP_TIME: constant(uint256) = 86400 +MIN_ADMIN_FEE_CLAIM_INTERVAL: constant(uint256) = 86400 MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 100 MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS @@ -240,7 +226,7 @@ def __init__( _math: address, _weth: address, _salt: bytes32, - packed_precisions: uint256, + __packed_precisions: uint256, packed_A_gamma: uint256, packed_fee_params: uint256, packed_rebalancing_params: uint256, @@ -250,14 +236,13 @@ def __init__( WETH20 = _weth MATH = Math(_math) - self.factory = msg.sender - + factory = Factory(msg.sender) name = _name symbol = _symbol coins = _coins - self.packed_precisions = packed_precisions # <------- Precisions of coins - # are calculated as 10**(18 - coin.decimals()). + PRECISIONS = self._unpack(__packed_precisions) # <-- Precisions of coins + # are calculated as 10**(18 - coin.decimals()). self.initial_A_gamma = packed_A_gamma # <------------------- A and gamma. self.future_A_gamma = packed_A_gamma @@ -274,6 +259,7 @@ def __init__( self.last_prices_packed = packed_prices self.last_prices_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. @@ -300,109 +286,83 @@ def __init__( # ------------------- Token transfers in and out of the AMM ------------------ -@payable -@external -def __default__(): - if msg.value > 0: - assert WETH20 in coins - - @internal def _transfer_in( - _coin: address, - dx: uint256, - dy: uint256, - mvalue: uint256, - callbacker: address, - callback_sig: bytes32, + _coin_idx: uint256, + _dx: uint256, sender: address, - receiver: address, - use_eth: bool -): + expect_optimistic_transfer: bool, +) -> uint256: """ @notice Transfers `_coin` from `sender` to `self` and calls `callback_sig` if it is not empty. - @dev The callback sig must have the following args: - sender: address - receiver: address - coin: address - dx: uint256 - dy: uint256 - @params _coin address of the coin to transfer in. + @params _coin_idx uint256 Index of the coin to transfer in. @params dx amount of `_coin` to transfer into the pool. - @params dy amount of `_coin` to transfer out of the pool. - @params mvalue msg.value if the transfer is ETH, 0 otherwise. - @params callbacker address to call `callback_sig` on. - @params callback_sig signature of the callback function. @params sender address to transfer `_coin` from. - @params receiver address to transfer `_coin` to. - @params use_eth True if the transfer is ETH, False otherwise. - """ - - if use_eth and _coin == WETH20: - assert mvalue == dx # dev: incorrect eth amount - else: - assert mvalue == 0 # dev: nonzero eth amount - - if callback_sig == empty(bytes32): - - assert ERC20(_coin).transferFrom( - sender, self, dx, default_return_value=True - ) - - else: - - # --------- This part of the _transfer_in logic is only accessible - # by _exchange. - - # First call callback logic and then check if pool - # gets dx amounts of _coins[i], revert otherwise. - b: uint256 = ERC20(_coin).balanceOf(self) - raw_call( - callbacker, - concat( - slice(callback_sig, 0, 4), - _abi_encode(sender, receiver, _coin, dx, dy) - ) - ) - assert ERC20(_coin).balanceOf(self) - b == dx # dev: callback didn't give us coins - # ^------ note: dx cannot - # be 0, so the contract MUST receive some _coin. + @params expect_optimistic_transfer bool True if pool expects user to transfer. + This is only enabled for exchange_received. + @return The amount of tokens received. + """ + 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 + # the pool before calling this method. + + # If someone donates extra tokens to the contract: do not acknowledge. + # We only want to know if there are dx amount of tokens. Anything extra, + # we ignore. This is why we need to check if received_amounts (which + # accounts for coin balances of the contract) is atleast dx. + # If we checked for received_amounts == dx, an extra transfer without a + # call to exchange_received will break the method. + dx: uint256 = coin_balance - self.balances[_coin_idx] + assert dx >= _dx # dev: user didn't give us coins + + # Adjust balances + self.balances[_coin_idx] += dx + + return dx + + # ----------------------------------------------- ERC20 transferFrom flow. + + # EXTERNAL CALL + assert ERC20(coins[_coin_idx]).transferFrom( + sender, + self, + _dx, + default_return_value=True + ) - if _coin == WETH20: - WETH(WETH20).withdraw(dx) # <--------- if WETH was transferred in - # previous step and `not use_eth`, withdraw WETH to ETH. + dx: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance + self.balances[_coin_idx] += dx + return dx @internal -def _transfer_out( - _coin: address, _amount: uint256, use_eth: bool, receiver: address -): +def _transfer_out(_coin_idx: uint256, _amount: uint256, receiver: address): """ @notice Transfer a single token from the pool to receiver. @dev This function is called by `remove_liquidity` and - `remove_liquidity_one` and `_exchange` methods. - @params _coin Address of the token to transfer out + `remove_liquidity_one`, `_claim_admin_fees` and `_exchange` methods. + @params _coin_idx uint256 Index of the token to transfer out @params _amount Amount of token to transfer out - @params use_eth Whether to transfer ETH or not @params receiver Address to send the tokens to """ - if use_eth and _coin == WETH20: - raw_call(receiver, b"", value=_amount) - else: - if _coin == WETH20: - WETH(WETH20).deposit(value=_amount) + # Adjust balances before handling transfers: + self.balances[_coin_idx] -= _amount - assert ERC20(_coin).transfer( - receiver, _amount, default_return_value=True - ) + # EXTERNAL CALL + assert ERC20(coins[_coin_idx]).transfer( + receiver, + _amount, + default_return_value=True + ) # -------------------------- AMM Main Functions ------------------------------ -@payable @external @nonreentrant("lock") def exchange( @@ -410,7 +370,6 @@ def exchange( j: uint256, dx: uint256, min_dy: uint256, - use_eth: bool = False, receiver: address = msg.sender ) -> uint256: """ @@ -419,107 +378,65 @@ def exchange( @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 use_eth True if the input coin is native token, False otherwise @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( msg.sender, - msg.value, i, j, dx, min_dy, - use_eth, receiver, - empty(address), - empty(bytes32) + False, ) -@payable @external @nonreentrant('lock') -def exchange_underlying( +def exchange_received( i: uint256, j: uint256, dx: uint256, min_dy: uint256, - receiver: address = msg.sender + receiver: address = msg.sender, ) -> uint256: """ - @notice Exchange using native token transfers. + @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`. + @dev Use-case is to reduce the number of redundant ERC20 token + transfers in zaps. Primarily for dex-aggregators/arbitrageurs/searchers. + 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 receiver Address to send the output coin to. Default is msg.sender - @return uint256 Amount of tokens at index j received by the `receiver + @param receiver Address to send the output coin to + @return uint256 Amount of tokens at index j received by the `receiver` """ return self._exchange( msg.sender, - msg.value, i, j, dx, min_dy, - True, receiver, - empty(address), - empty(bytes32) + True, ) -@external -@nonreentrant('lock') -def exchange_extended( - i: uint256, - j: uint256, - dx: uint256, - min_dy: uint256, - use_eth: bool, - sender: address, - receiver: address, - cb: bytes32 -) -> uint256: - """ - @notice Exchange with callback method. - @dev This method does not allow swapping in native token, but does allow - swaps that transfer out native token from the pool. - @dev Does not allow flashloans - @dev One use-case is to reduce the number of redundant ERC20 token - transfers in zaps. - @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 use_eth True if output is native token, False otherwise - @param sender Address to transfer input coin from - @param receiver Address to send the output coin to - @param cb Callback signature - @return uint256 Amount of tokens at index j received by the `receiver` - """ - - assert cb != empty(bytes32) # dev: No callback specified - return self._exchange( - sender, 0, i, j, dx, min_dy, use_eth, receiver, msg.sender, cb - ) # callbacker should never be self ------------------^ - - -@payable @external @nonreentrant("lock") def add_liquidity( amounts: uint256[N_COINS], min_mint_amount: uint256, - use_eth: bool = False, receiver: address = msg.sender ) -> uint256: """ @notice Adds liquidity into the pool. @param amounts Amounts of each coin to add. @param min_mint_amount Minimum amount of LP to mint. - @param use_eth True if native token is being added to the pool. @param receiver Address to send the LP tokens to. Default is msg.sender @return uint256 Amount of LP tokens received by the `receiver """ @@ -527,7 +444,6 @@ def add_liquidity( A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) - xx: uint256[N_COINS] = empty(uint256[N_COINS]) d_token: uint256 = 0 d_token_fee: uint256 = 0 old_D: uint256 = 0 @@ -536,61 +452,38 @@ def add_liquidity( # --------------------- Get prices, balances ----------------------------- - precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) packed_price_scale: uint256 = self.price_scale_packed price_scale: uint256[N_COINS-1] = self._unpack_prices(packed_price_scale) # -------------------------------------- Update balances and calculate xp. xp_old: uint256[N_COINS] = xp - for i in range(N_COINS): - bal: uint256 = xp[i] + amounts[i] - xp[i] = bal - self.balances[i] = bal - xx = xp - - xp[0] *= precisions[0] - xp_old[0] *= precisions[0] - for i in range(1, N_COINS): - xp[i] = unsafe_div(xp[i] * price_scale[i-1] * precisions[i], PRECISION) - xp_old[i] = unsafe_div( - xp_old[i] * unsafe_mul(price_scale[i-1], precisions[i]), - PRECISION - ) + amounts_received: uint256[N_COINS] = empty(uint256[N_COINS]) - # ---------------- transferFrom token into the pool ---------------------- + ########################## TRANSFER IN <------- for i in range(N_COINS): - if amounts[i] > 0: + # Updates self.balances here: + amounts_received[i] = self._transfer_in( + i, + amounts[i], + msg.sender, + False, # <--------------------- Disable optimistic transfers. + ) + xp[i] = xp[i] + amounts_received[i] - if coins[i] == WETH20: - - self._transfer_in( - coins[i], - amounts[i], - 0, # <----------------------------------- - msg.value, # | No callbacks - empty(address), # <----------------------| for - empty(bytes32), # <----------------------| add_liquidity. - msg.sender, # | - empty(address), # <----------------------- - use_eth - ) - - else: + xp[0] *= PRECISIONS[0] + xp_old[0] *= PRECISIONS[0] + for i in range(N_COINS): - self._transfer_in( - coins[i], - amounts[i], - 0, - 0, # <----------------- mvalue = 0 if coin is not WETH20. - empty(address), - empty(bytes32), - msg.sender, - empty(address), - False # <-------- use_eth is False if coin is not WETH20. - ) + if i >= 1: + xp[i] = unsafe_div(xp[i] * price_scale[i-1] * PRECISIONS[i], PRECISION) + xp_old[i] = unsafe_div( + xp_old[i] * unsafe_mul(price_scale[i-1], PRECISIONS[i]), + PRECISION + ) + if amounts_received[i] > 0: amountsp[i] = xp[i] - xp_old[i] # -------------------- Calculate LP tokens to mint ----------------------- @@ -610,7 +503,7 @@ def add_liquidity( if old_D > 0: d_token = token_supply * D / old_D - token_supply else: - d_token = self.get_xcp(D) # <------------------------- Making initial + d_token = self.get_xcp(D, packed_price_scale) # <----- Making initial # virtual price equal to 1. assert d_token > 0 # dev: nothing minted @@ -629,19 +522,27 @@ def add_liquidity( else: + # (re)instatiating an empty pool: + self.D = D self.virtual_price = 10**18 self.xcp_profit = 10**18 self.xcp_profit_a = 10**18 + + # Initialise xcp oracle here: + self.cached_xcp_oracle = d_token # <--- virtual_price * totalSupply / 10**18 + self.mint(receiver, d_token) assert d_token >= min_mint_amount, "Slippage" + # ---------------------------------------------- Log and claim admin fees. + log AddLiquidity( - receiver, amounts, d_token_fee, token_supply, packed_price_scale + receiver, amounts_received, d_token_fee, token_supply, packed_price_scale ) - self._claim_admin_fees() # <--------------------------- Claim admin fees. + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. return d_token @@ -651,27 +552,19 @@ def add_liquidity( def remove_liquidity( _amount: uint256, min_amounts: uint256[N_COINS], - use_eth: bool = False, receiver: address = msg.sender, - claim_admin_fees: bool = True, ) -> uint256[N_COINS]: """ @notice This withdrawal method is very safe, does no complex math since tokens are withdrawn in balanced proportions. No fees are charged. @param _amount Amount of LP tokens to burn @param min_amounts Minimum amounts of tokens to withdraw - @param use_eth Whether to withdraw ETH or not @param receiver Address to send the withdrawn tokens to - @param claim_admin_fees If True, call self._claim_admin_fees(). Default is True. @return uint256[3] Amount of pool tokens received by the `receiver` """ amount: uint256 = _amount balances: uint256[N_COINS] = self.balances - d_balances: uint256[N_COINS] = empty(uint256[N_COINS]) - - if claim_admin_fees: - self._claim_admin_fees() # <------ We claim fees so that the DAO gets - # paid before withdrawal. In emergency cases, set it to False. + withdraw_amounts: uint256[N_COINS] = empty(uint256[N_COINS]) # -------------------------------------------------------- Burn LP tokens. @@ -691,18 +584,16 @@ def remove_liquidity( for i in range(N_COINS): - d_balances[i] = balances[i] - self.balances[i] = 0 # <------------------------- Empty the pool. + withdraw_amounts[i] = balances[i] else: # <-------------------------------------------------------- Case 1. amount -= 1 # <---- To prevent rounding errors, favor LPs a tiny bit. for i in range(N_COINS): - d_balances[i] = balances[i] * amount / total_supply - assert d_balances[i] >= min_amounts[i] - self.balances[i] = balances[i] - d_balances[i] - balances[i] = d_balances[i] # <-- Now it's the amounts going out. + + withdraw_amounts[i] = balances[i] * amount / total_supply + assert withdraw_amounts[i] >= min_amounts[i] D: uint256 = self.D self.D = D - unsafe_div(D * amount, total_supply) # <----------- Reduce D @@ -713,11 +604,13 @@ def remove_liquidity( # ---------------------------------- Transfers --------------------------- for i in range(N_COINS): - self._transfer_out(coins[i], d_balances[i], use_eth, receiver) + # _transfer_out updates self.balances here. Update to state occurs + # before external calls: + self._transfer_out(i, withdraw_amounts[i], receiver) - log RemoveLiquidity(msg.sender, balances, total_supply - _amount) + log RemoveLiquidity(msg.sender, withdraw_amounts, total_supply - _amount) - return d_balances + return withdraw_amounts @external @@ -726,7 +619,6 @@ def remove_liquidity_one_coin( token_amount: uint256, i: uint256, min_amount: uint256, - use_eth: bool = False, receiver: address = msg.sender ) -> uint256: """ @@ -736,7 +628,6 @@ def remove_liquidity_one_coin( @param token_amount Amount of LP tokens to burn @param i Index of the token to withdraw @param min_amount Minimum amount of token to withdraw. - @param use_eth Whether to withdraw ETH or not @param receiver Address to send the withdrawn tokens to @return Amount of tokens at index i received by the `receiver` """ @@ -749,9 +640,6 @@ def remove_liquidity_one_coin( xp: uint256[N_COINS] = empty(uint256[N_COINS]) approx_fee: uint256 = 0 - # ---------------------------- Claim admin fees before removing liquidity. - self._claim_admin_fees() - # ------------------------------------------------------------------------ dy, D, xp, approx_fee = self._calc_withdraw_one_coin( @@ -763,36 +651,34 @@ def remove_liquidity_one_coin( assert dy >= min_amount, "Slippage" - # ------------------------- Transfers ------------------------------------ + # ---------------------------- State Updates ----------------------------- - self.balances[i] -= dy + # Burn user's tokens: self.burnFrom(msg.sender, token_amount) - self._transfer_out(coins[i], dy, use_eth, receiver) 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 + # external calls: + self._transfer_out(i, dy, receiver) + log RemoveLiquidityOne( msg.sender, token_amount, i, dy, approx_fee, packed_price_scale ) - return dy - + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. -@external -@nonreentrant("lock") -def claim_admin_fees(): - """ - @notice Claim admin fees. Callable by anyone. - """ - self._claim_admin_fees() + return dy # -------------------------- Packing functions ------------------------------- @internal -@view +@pure def _pack(x: uint256[3]) -> uint256: """ @notice Packs 3 integers with values <= 10**18 into a uint256 @@ -803,7 +689,7 @@ def _pack(x: uint256[3]) -> uint256: @internal -@view +@pure def _unpack(_packed: uint256) -> uint256[3]: """ @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) @@ -818,7 +704,7 @@ def _unpack(_packed: uint256) -> uint256[3]: @internal -@view +@pure def _pack_prices(prices_to_pack: uint256[N_COINS-1]) -> uint256: """ @notice Packs N_COINS-1 prices into a uint256. @@ -836,7 +722,7 @@ def _pack_prices(prices_to_pack: uint256[N_COINS-1]) -> uint256: @internal -@view +@pure def _unpack_prices(_packed_prices: uint256) -> uint256[2]: """ @notice Unpacks N_COINS-1 prices from a uint256. @@ -858,15 +744,12 @@ def _unpack_prices(_packed_prices: uint256) -> uint256[2]: @internal def _exchange( sender: address, - mvalue: uint256, i: uint256, j: uint256, dx: uint256, min_dy: uint256, - use_eth: bool, receiver: address, - callbacker: address, - callback_sig: bytes32 + expect_optimistic_transfer: bool, ) -> uint256: assert i != j # dev: coin index out of range @@ -874,27 +757,36 @@ def _exchange( A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances - precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) 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. - xp[i] = x0 + dx - self.balances[i] = xp[i] + + ########################## 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 packed_price_scale: uint256 = self.price_scale_packed price_scale: uint256[N_COINS - 1] = self._unpack_prices( packed_price_scale ) - xp[0] *= precisions[0] + xp[0] *= PRECISIONS[0] for k in range(1, N_COINS): xp[k] = unsafe_div( - xp[k] * price_scale[k - 1] * precisions[k], + xp[k] * price_scale[k - 1] * PRECISIONS[k], PRECISION ) # <-------- Safu to do unsafe_div here since PRECISION is not zero. - prec_i: uint256 = precisions[i] + prec_i: uint256 = PRECISIONS[i] # ----------- Update invariant if A, gamma are undergoing ramps --------- @@ -914,7 +806,7 @@ def _exchange( # ----------------------- Calculate dy and fees -------------------------- D: uint256 = self.D - prec_j: uint256 = precisions[j] + prec_j: uint256 = PRECISIONS[j] y_out: uint256[2] = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, j) dy = xp[j] - y_out[0] xp[j] -= dy @@ -930,30 +822,25 @@ def _exchange( assert dy >= min_dy, "Slippage" y -= dy - self.balances[j] = y # <----------- Update pool balance of outgoing coin. y *= prec_j if j > 0: y = unsafe_div(y * price_scale[j - 1], PRECISION) xp[j] = y # <------------------------------------------------- Update xp. - # ---------------------- Do Transfers in and out ------------------------- + # ------ Tweak price_scale with good initial guess for newton_D ---------- - ########################## TRANSFER IN <------- - self._transfer_in( - coins[i], dx, dy, mvalue, - callbacker, callback_sig, # <-------- Callback method is called here. - sender, receiver, use_eth, - ) + packed_price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1]) - ########################## -------> TRANSFER OUT - self._transfer_out(coins[j], dy, use_eth, receiver) + # --------------------------- Do Transfers out --------------------------- - # ------ Tweak price_scale with good initial guess for newton_D ---------- + ########################## -------> TRANSFER OUT - packed_price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1]) + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(j, dy, receiver) - log TokenExchange(sender, i, dx, j, dy, fee, packed_price_scale) + log TokenExchange(sender, i, dx_received, j, dy, fee, packed_price_scale) return dy @@ -966,7 +853,7 @@ def tweak_price( K0_prev: uint256 = 0, ) -> uint256: """ - @notice Tweaks price_oracle, last_price and conditionally adjusts + @notice Updates price_oracle, last_price and conditionally adjusts price_scale. This is called whenever there is an unbalanced liquidity operation: _exchange, add_liquidity, or remove_liquidity_one_coin. @@ -979,28 +866,21 @@ def tweak_price( # ---------------------------- Read storage ------------------------------ - rebalancing_params: uint256[3] = self._unpack( - self.packed_rebalancing_params - ) # <---------- Contains: allowed_extra_profit, adjustment_step, ma_time. - price_oracle: uint256[N_COINS - 1] = self._unpack_prices( - self.price_oracle_packed - ) - last_prices: uint256[N_COINS - 1] = self._unpack_prices( - self.last_prices_packed - ) + price_oracle: uint256[N_COINS - 1] = self._unpack_prices(self.price_oracle_packed) + last_prices: uint256[N_COINS - 1] = self._unpack_prices(self.last_prices_packed) packed_price_scale: uint256 = self.price_scale_packed - price_scale: uint256[N_COINS - 1] = self._unpack_prices( - packed_price_scale - ) + price_scale: uint256[N_COINS - 1] = self._unpack_prices(packed_price_scale) + rebalancing_params: uint256[3] = self._unpack(self.packed_rebalancing_params) + # Contains: allowed_extra_profit, adjustment_step, ma_time. -----^ total_supply: uint256 = self.totalSupply old_xcp_profit: uint256 = self.xcp_profit old_virtual_price: uint256 = self.virtual_price - last_prices_timestamp: uint256 = self.last_prices_timestamp - # ----------------------- Update MA if needed ---------------------------- + # ----------------------- Update Oracles if needed ----------------------- - if last_prices_timestamp < block.timestamp: + last_timestamp: uint256 = self.last_prices_timestamp + 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 @@ -1011,13 +891,15 @@ def tweak_price( alpha: uint256 = MATH.wad_exp( -convert( unsafe_div( - (block.timestamp - last_prices_timestamp) * 10**18, + (block.timestamp - last_timestamp) * 10**18, rebalancing_params[2] # <----------------------- ma_time. ), int256, ) ) + # ---------------------------------------------- Update price oracles. + for k in range(N_COINS - 1): # ----------------- We cap state price that goes into the EMA with @@ -1029,11 +911,32 @@ def tweak_price( ) self.price_oracle_packed = self._pack_prices(price_oracle) - self.last_prices_timestamp = block.timestamp # <---- Store 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 to be done to the price_scale. + # ------------------------------------------------- Update xcp oracle. + + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + alpha = MATH.wad_exp( + -convert( + unsafe_div( + (block.timestamp - last_timestamp) * 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: + self.last_prices_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 + # to be done to the price_scale. + # ------------------------------------------------------------------------ # ------------------ If new_D is set to 0, calculate it ------------------ @@ -1076,6 +979,10 @@ def tweak_price( 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) + self.xcp_profit = xcp_profit # ------------ Rebalance liquidity if there's enough profits to adjust it: @@ -1169,35 +1076,47 @@ def tweak_price( def _claim_admin_fees(): """ @notice Claims admin fees and sends it to fee_receiver set in the factory. + @dev Functionally similar to: + 1. Calculating admin's share of fees, + 2. minting LP tokens, + 3. admin claims underlying tokens via remove_liquidity. """ + + # --------------------- Check if fees can be claimed --------------------- + + # Disable fee claiming if: + # 1. If time passed since last fee claim is less than + # MIN_ADMIN_FEE_CLAIM_INTERVAL. + # 2. Pool parameters are being ramped. + + last_claim_time: uint256 = self.last_admin_fee_claim_timestamp + if ( + block.timestamp - last_claim_time < MIN_ADMIN_FEE_CLAIM_INTERVAL or + self.future_A_gamma_time > block.timestamp + ): + 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. - total_supply: uint256 = self.totalSupply + 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 # 2. there are less than 10**18 (or 1 unit of) lp tokens, else it can lead # to manipulated virtual prices. - if xcp_profit <= xcp_profit_a or total_supply < 10**18: - return - - # Claim tokens belonging to the admin here. This is done by 'gulping' - # pool tokens that have accrued as fees, but not accounted in pool's - # `self.balances` yet: pool balances only account for incoming and - # outgoing tokens excluding fees. Following 'gulps' fees: - for i in range(N_COINS): - if coins[i] == WETH20: - self.balances[i] = self.balance - else: - self.balances[i] = ERC20(coins[i]).balanceOf(self) + if xcp_profit <= xcp_profit_a or current_lp_token_supply < 10**18: + return - # If the pool has made no profits, `xcp_profit == xcp_profit_a` - # and the pool gulps nothing in the previous step. + # ---------- Conditions met to claim admin fees: compute state. ---------- vprice: uint256 = self.virtual_price + packed_price_scale: uint256 = self.price_scale_packed + fee_receiver: address = factory.fee_receiver() + balances: uint256[N_COINS] = self.balances # Admin fees are calculated as follows. # 1. Calculate accrued profit since last claim. `xcp_profit` @@ -1214,42 +1133,75 @@ def _claim_admin_fees(): # ------------------------------ Claim admin fees by minting admin's share # of the pool in LP tokens. - receiver: address = Factory(self.factory).fee_receiver() - if receiver != empty(address) and fees > 0: + admin_share: uint256 = 0 + frac: uint256 = 0 + if fee_receiver != empty(address) and fees > 0: - frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18 - claimed: uint256 = self.mint_relative(receiver, frac) + # -------------------------------- Calculate admin share to be minted. + frac = vprice * 10**18 / (vprice - fees) - 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 - self.xcp_profit = xcp_profit + # ------------------- Recalculate virtual_price following admin fee claim. + total_supply_including_admin_share: uint256 = ( + current_lp_token_supply + admin_share + ) + vprice = ( + 10**18 * self.get_xcp(D, packed_price_scale) / + total_supply_including_admin_share + ) + + # Do not claim fees if doing so causes virtual price to drop below 10**18. + if vprice < 10**18: + return - log ClaimAdminFee(receiver, claimed) + # ---------------------------- Update State ------------------------------ - # ------------------------------------------- Recalculate D b/c we gulped. - D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], self.xp(), 0) - self.D = D + self.xcp_profit = xcp_profit + self.last_admin_fee_claim_timestamp = block.timestamp - # ------------------- Recalculate virtual_price following admin fee claim. - # In this instance we do not check if current virtual price is greater - # than old virtual price, since the claim process can result - # in a small decrease in pool's value. + # Since we reduce balances: virtual price goes down + self.virtual_price = vprice - self.virtual_price = 10**18 * self.get_xcp(D) / self.totalSupply - self.xcp_profit_a = xcp_profit # <------------ Cache last claimed profit. + # Adjust D after admin seemingly removes liquidity + self.D = D - unsafe_div(D * admin_share, total_supply_including_admin_share) + if xcp_profit > xcp_profit_a: + self.xcp_profit_a = xcp_profit # <-------- Cache last claimed profit. -@internal -@view -def xp() -> uint256[N_COINS]: + # --------------------------- Handle Transfers --------------------------- - result: uint256[N_COINS] = self.balances - packed_prices: uint256 = self.price_scale_packed - precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + admin_tokens: uint256[N_COINS] = empty(uint256[N_COINS]) + if admin_share > 0: + + for i in range(N_COINS): + + admin_tokens[i] = ( + balances[i] * admin_share / + total_supply_including_admin_share + ) + + # _transfer_out tokens to admin and update self.balances. State + # update to self.balances occurs before external contract calls: + self._transfer_out(i, admin_tokens[i], fee_receiver) + + log ClaimAdminFee(fee_receiver, admin_tokens) + + +@internal +@pure +def xp( + balances: uint256[N_COINS], + price_scale_packed: uint256, +) -> uint256[N_COINS]: - result[0] *= precisions[0] + result: uint256[N_COINS] = balances + result[0] *= PRECISIONS[0] + packed_prices: uint256 = price_scale_packed for i in range(1, N_COINS): - p: uint256 = (packed_prices & PRICE_MASK) * precisions[i] + p: uint256 = (packed_prices & PRICE_MASK) * PRECISIONS[i] result[i] = result[i] * p / PRECISION packed_prices = packed_prices >> PRICE_SIZE @@ -1285,8 +1237,10 @@ def _A_gamma() -> uint256[2]: @internal @view def _fee(xp: uint256[N_COINS]) -> uint256: + fee_params: uint256[3] = self._unpack(self.packed_fee_params) f: uint256 = MATH.reduction_coefficient(xp, fee_params[2]) + return unsafe_div( fee_params[0] * f + fee_params[1] * (10**18 - f), 10**18 @@ -1294,12 +1248,12 @@ def _fee(xp: uint256[N_COINS]) -> uint256: @internal -@view -def get_xcp(D: uint256) -> uint256: +@pure +def get_xcp(D: uint256, price_scale_packed: uint256) -> uint256: x: uint256[N_COINS] = empty(uint256[N_COINS]) x[0] = D / N_COINS - packed_prices: uint256 = self.price_scale_packed # <-- No precisions here + packed_prices: uint256 = price_scale_packed # <------ No precisions here # because we don't switch to "real" units. for i in range(1, N_COINS): @@ -1348,13 +1302,12 @@ def _calc_withdraw_one_coin( assert i < N_COINS # dev: coin out of range xx: uint256[N_COINS] = self.balances - precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) - xp: uint256[N_COINS] = precisions + xp: uint256[N_COINS] = PRECISIONS D0: uint256 = 0 # -------------------------- Calculate D0 and xp ------------------------- - price_scale_i: uint256 = PRECISION * precisions[0] + price_scale_i: uint256 = PRECISION * PRECISIONS[0] packed_prices: uint256 = self.price_scale_packed xp[0] *= xx[0] for k in range(1, N_COINS): @@ -1479,9 +1432,6 @@ def approve(_spender: address, _value: uint256) -> bool: """ @notice Allow `_spender` to transfer up to `_value` amount of tokens from the caller's account. - @dev Non-zero to non-zero approvals are allowed, but should - be used cautiously. The methods increaseAllowance + decreaseAllowance - are available to prevent any front-running that may occur. @param _spender The account permitted to spend up to `_value` amount of caller's funds. @param _value The amount of tokens `_spender` is allowed to spend. @@ -1491,51 +1441,6 @@ def approve(_spender: address, _value: uint256) -> bool: return True -@external -def increaseAllowance(_spender: address, _add_value: uint256) -> bool: - """ - @notice Increase the allowance granted to `_spender`. - @dev This function will never overflow, and instead will bound - allowance to max_value(uint256). This has the potential to grant an - infinite approval. - @param _spender The account to increase the allowance of. - @param _add_value The amount to increase the allowance by. - @return bool Success - """ - cached_allowance: uint256 = self.allowance[msg.sender][_spender] - allowance: uint256 = unsafe_add(cached_allowance, _add_value) - - if allowance < cached_allowance: # <-------------- Check for an overflow. - allowance = max_value(uint256) - - if allowance != cached_allowance: - self._approve(msg.sender, _spender, allowance) - - return True - - -@external -def decreaseAllowance(_spender: address, _sub_value: uint256) -> bool: - """ - @notice Decrease the allowance granted to `_spender`. - @dev This function will never underflow, and instead will bound - allowance to 0. - @param _spender The account to decrease the allowance of. - @param _sub_value The amount to decrease the allowance by. - @return bool Success. - """ - cached_allowance: uint256 = self.allowance[msg.sender][_spender] - allowance: uint256 = unsafe_sub(cached_allowance, _sub_value) - - if cached_allowance < allowance: # <------------- Check for an underflow. - allowance = 0 - - if allowance != cached_allowance: - self._approve(msg.sender, _spender, allowance) - - return True - - @external def permit( _owner: address, @@ -1601,24 +1506,6 @@ def mint(_to: address, _value: uint256) -> bool: return True -@internal -def mint_relative(_to: address, frac: uint256) -> uint256: - """ - @dev Increases supply by factor of (1 + frac/1e18) and mints it for _to - @param _to The account that will receive the created tokens. - @param frac The fraction of the current supply to mint. - @return uint256 Amount of tokens minted. - """ - supply: uint256 = self.totalSupply - d_supply: uint256 = supply * frac / 10**18 - if d_supply > 0: - self.totalSupply = supply + d_supply - self.balanceOf[_to] += d_supply - log Transfer(empty(address), _to, d_supply) - - return d_supply - - @internal def burnFrom(_to: address, _value: uint256) -> bool: """ @@ -1644,7 +1531,7 @@ def fee_receiver() -> address: @notice Returns the address of the admin fee receiver. @return address Fee receiver. """ - return Factory(self.factory).fee_receiver() + return factory.fee_receiver() @external @@ -1658,7 +1545,7 @@ def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256: @param deposit True if it is a deposit action, False if withdrawn. @return uint256 Amount of LP tokens deposited or withdrawn. """ - view_contract: address = Factory(self.factory).views_implementation() + view_contract: address = factory.views_implementation() return Views(view_contract).calc_token_amount(amounts, deposit, self) @@ -1673,7 +1560,7 @@ def get_dy(i: uint256, j: uint256, dx: uint256) -> uint256: @param dx amount of input coin[i] tokens @return uint256 Exact amount of output j tokens for dx amount of i input tokens. """ - view_contract: address = Factory(self.factory).views_implementation() + view_contract: address = factory.views_implementation() return Views(view_contract).get_dy(i, j, dx, self) @@ -1691,7 +1578,7 @@ def get_dx(i: uint256, j: uint256, dy: uint256) -> uint256: @param dy amount of input coin[j] tokens received @return uint256 Approximate amount of input i tokens to get dy amount of j tokens. """ - view_contract: address = Factory(self.factory).views_implementation() + view_contract: address = factory.views_implementation() return Views(view_contract).get_dx(i, j, dy, self) @@ -1705,9 +1592,7 @@ def lp_price() -> uint256: @return uint256 LP price. """ - price_oracle: uint256[N_COINS-1] = self._unpack_prices( - self.price_oracle_packed - ) + price_oracle: uint256[N_COINS-1] = self._unpack_prices(self.price_oracle_packed) return ( 3 * self.virtual_price * MATH.cbrt(price_oracle[0] * price_oracle[1]) ) / 10**24 @@ -1723,7 +1608,10 @@ def get_virtual_price() -> uint256: virtual price. @return uint256 Virtual Price. """ - return 10**18 * self.get_xcp(self.D) / self.totalSupply + return ( + 10**18 * self.get_xcp(self.D, self.price_scale_packed) / + self.totalSupply + ) @external @@ -1764,6 +1652,38 @@ def price_oracle(k: uint256) -> uint256: return 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.last_prices_timestamp + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + + if last_prices_timestamp < block.timestamp: + + alpha: uint256 = MATH.wad_exp( + -convert( + (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 def last_prices(k: uint256) -> uint256: @@ -1783,6 +1703,7 @@ def last_prices(k: uint256) -> uint256: @external @view +@nonreentrant("lock") def price_scale(k: uint256) -> uint256: """ @notice Returns the price scale of the coin at index `k` w.r.t the coin @@ -1805,7 +1726,7 @@ def fee() -> uint256: removed. @return uint256 fee bps. """ - return self._fee(self.xp()) + return self._fee(self.xp(self.balances, self.price_scale_packed)) @view @@ -1929,7 +1850,7 @@ def precisions() -> uint256[N_COINS]: # <-------------- For by view contract. @notice Returns the precisions of each coin in the pool. @return uint256[3] precisions of coins. """ - return self._unpack(self.packed_precisions) + return PRECISIONS @external @@ -1967,7 +1888,7 @@ def ramp_A_gamma( @param future_gamma The future gamma value. @param future_time The timestamp at which the ramping will end. """ - assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert msg.sender == factory.admin() # dev: only owner assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time @@ -2012,7 +1933,7 @@ def stop_ramp_A_gamma(): @notice Stop Ramping A and gamma parameters immediately. @dev Only accessible by factory admin. """ - assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert msg.sender == factory.admin() # dev: only owner A_gamma: uint256[2] = self._A_gamma() current_A_gamma: uint256 = A_gamma[0] << 128 @@ -2028,13 +1949,15 @@ def stop_ramp_A_gamma(): @external -def commit_new_parameters( +@nonreentrant('lock') +def apply_new_parameters( _new_mid_fee: uint256, _new_out_fee: uint256, _new_fee_gamma: uint256, _new_allowed_extra_profit: uint256, _new_adjustment_step: uint256, _new_ma_time: uint256, + _new_xcp_ma_time: uint256, ): """ @notice Commit new parameters. @@ -2045,12 +1968,9 @@ def commit_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(self.factory).admin() # dev: only owner - assert self.admin_actions_deadline == 0 # dev: active action - - _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY - self.admin_actions_deadline = _deadline + assert msg.sender == factory.admin() # dev: only owner # ----------------------------- Set fee params --------------------------- @@ -2074,9 +1994,7 @@ def commit_new_parameters( else: new_fee_gamma = current_fee_params[2] - self.future_packed_fee_params = self._pack( - [new_mid_fee, new_out_fee, new_fee_gamma] - ) + self.packed_fee_params = self._pack([new_mid_fee, new_out_fee, new_fee_gamma]) # ----------------- Set liquidity rebalancing parameters ----------------- @@ -2097,60 +2015,26 @@ def commit_new_parameters( else: new_ma_time = current_rebalancing_params[2] - self.future_packed_rebalancing_params = self._pack( + self.packed_rebalancing_params = self._pack( [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 CommitNewParameters( - _deadline, + log NewParameters( new_mid_fee, new_out_fee, new_fee_gamma, new_allowed_extra_profit, new_adjustment_step, new_ma_time, + _new_xcp_ma_time, ) - - -@external -@nonreentrant("lock") -def apply_new_parameters(): - """ - @notice Apply committed parameters. - @dev Only callable after admin_actions_deadline. - """ - assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time - assert self.admin_actions_deadline != 0 # dev: no active action - - self.admin_actions_deadline = 0 - - packed_fee_params: uint256 = self.future_packed_fee_params - self.packed_fee_params = packed_fee_params - - packed_rebalancing_params: uint256 = self.future_packed_rebalancing_params - self.packed_rebalancing_params = packed_rebalancing_params - - rebalancing_params: uint256[3] = self._unpack(packed_rebalancing_params) - fee_params: uint256[3] = self._unpack(packed_fee_params) - - log NewParameters( - fee_params[0], - fee_params[1], - fee_params[2], - rebalancing_params[0], - rebalancing_params[1], - rebalancing_params[2], - ) - - -@external -def revert_new_parameters(): - """ - @notice Revert committed parameters - @dev Only accessible by factory admin. Setting admin_actions_deadline to 0 - ensures a revert in apply_new_parameters. - """ - assert msg.sender == Factory(self.factory).admin() # dev: only owner - self.admin_actions_deadline = 0 diff --git a/scripts/deploy.py b/scripts/deploy.py index 25fb9b30..0d0b3464 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -27,7 +27,7 @@ "views": "0x06452f9c013fc37169B57Eab8F50A7A48c9198A3", "amm_impl": "0xd7E72f3615aa65b92A4DBdC211E296a35512988B", }, - "mainnet-fork": { + "ethereum:mainnet-fork": { "factory": "0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963", "math": "0xcBFf3004a20dBfE2731543AA38599A526e0fD6eE", "views": "0x064253915b8449fdEFac2c4A74aA9fdF56691a31", @@ -678,3 +678,48 @@ def deploy_gauge_and_set_up_vote(network, account, pool, factory): Contract(deploy_utils.GAUGE_CONTROLLER).gauge_types(gauge.address) == 5 ) + + +# ------ Deploy and set up new AMM impl ------- + + +@cli.command(cls=NetworkBoundCommand) +@network_option() +@account_option() +def deploy_amm_impl(network, account): + + deploy_utils.deploy_blueprint(project.CurveTricryptoOptimizedWETH, account) + + +@cli.command(cls=NetworkBoundCommand) +@network_option() +@account_option() +@click.option("--impl_address", required=True, type=str) +def set_new_amm_impl_dao(network, account, impl_address): + + assert "ethereum:mainnet" in network + is_sim = "mainnet-fork" in network + + if is_sim: + account = accounts["0xbabe61887f1de2713c6f97e567623453d3c79f67"] + + amm_impl = project.CurveTricryptoOptimizedWETH.at(impl_address) + factory = project.CurveTricryptoFactory.at( + DEPLOYED_CONTRACTS[network]["factory"] + ) + ID = 0 + + logger.info("Setting new impl for ethereum tricrypto factory:") + vote_id_gauge = make_vote( + deploy_utils.CURVE_DAO_OWNERSHIP, + [ + (factory.address, "set_pool_implementation", amm_impl.address, ID), + ], + "Replace Existing AMM impl with a non-ETH impl", + account, + ) + + if is_sim: + + simulate(vote_id_gauge, deploy_utils.CURVE_DAO_OWNERSHIP["voting"]) + assert factory.pool_implementations(ID) == amm_impl.address diff --git a/scripts/deployment_utils.py b/scripts/deployment_utils.py index d84bda19..209f5581 100644 --- a/scripts/deployment_utils.py +++ b/scripts/deployment_utils.py @@ -351,20 +351,6 @@ def test_deployment(pool, coins, fee_receiver, account): assert coin_contract.balanceOf(account) == coin_balance + dy_coin logger.info(f"Removed {dy_coin} of {coin_name}.") - logger.info("------------------------------ Claim admin fees") - logger.info("(should not claim since pool hasn't accrued enough profits)") - - fees_claimed = pool.balanceOf(fee_receiver) - pool.claim_admin_fees(sender=account, gas_limit=400000, **_get_tx_params()) - if pool.totalSupply() < 10**18: - assert pool.balanceOf(fee_receiver) == fees_claimed - logger.info("No fees claimed.") - else: - assert pool.balanceOf(fee_receiver) > fees_claimed - logger.info( - f"{pool.balanceOf(fee_receiver) - fees_claimed} LP tokens of admin fees claimed!" # noqa: E501 - ) - logger.info( "------------------------------ Remove liquidity proportionally" ) diff --git a/scripts/experiments/sim_dydx.py b/scripts/experiments/sim_dydx.py index e2f38abb..e70c9d37 100644 --- a/scripts/experiments/sim_dydx.py +++ b/scripts/experiments/sim_dydx.py @@ -72,7 +72,13 @@ def _get_dydx(swap, i, j): A = ANN / 10**4 / 3**3 gamma = swap.gamma() / 10**18 - xp = swap.internal.xp() + balances = [] + for i in range(3): + balances.append(swap.balances(i)) + + xp = swap.internal.xp( + balances, swap._storage.price_scale_packed.get(), swap.precisions() + ) for k in range(3): if k != i and k != j: diff --git a/tests/boa/fixtures/pool.py b/tests/boa/fixtures/pool.py index e69ed8e9..3e3c5a0f 100644 --- a/tests/boa/fixtures/pool.py +++ b/tests/boa/fixtures/pool.py @@ -46,7 +46,6 @@ def _crypto_swap_with_deposit( @pytest.fixture(scope="module") def params(): - ma_time = 866 # 600 seconds / ln(2) return { "A": 135 * 3**3 * 10000, "gamma": int(7e-5 * 1e18), @@ -55,7 +54,8 @@ def params(): "allowed_extra_profit": 2 * 10**12, "fee_gamma": int(0.01 * 1e18), "adjustment_step": int(0.0015 * 1e18), - "ma_time": ma_time, + "ma_time": 866, # # 600 seconds//math.log(2) + "xcp_ma_time": 62324, # 12 hours//math.log(2) "initial_prices": INITIAL_PRICES[1:], } diff --git a/tests/boa/profiling/test_boa_profile.py b/tests/boa/profiling/test_boa_profile.py index 468c1667..e7972b39 100644 --- a/tests/boa/profiling/test_boa_profile.py +++ b/tests/boa/profiling/test_boa_profile.py @@ -19,12 +19,7 @@ def _random_exchange(swap): i, j = _choose_indices() amount = int(swap.balances(i) * 0.01) - use_eth = i == 2 - value = 0 - if use_eth: - value = amount - - swap.exchange(i, j, amount, 0, use_eth, value=value) + swap.exchange(i, j, amount, 0) boa.env.time_travel(random.randint(12, 600)) @@ -34,7 +29,7 @@ def _random_deposit(swap): c = random.uniform(0, 0.05) amounts = [int(c * i * random.uniform(0, 0.8)) for i in balances] - swap.add_liquidity(amounts, 0, True, value=amounts[2]) + swap.add_liquidity(amounts, 0) boa.env.time_travel(random.randint(12, 600)) @@ -44,7 +39,7 @@ def _random_deposit_weth(swap): balances = [swap.balances(i) for i in range(3)] c = random.uniform(0, 0.05) amounts = [int(c * i * random.uniform(0, 0.8)) for i in balances] - swap.add_liquidity(amounts, 0, False) + swap.add_liquidity(amounts, 0) boa.env.time_travel(random.randint(12, 600)) @@ -52,34 +47,12 @@ def _random_deposit_one(swap): balances = [swap.balances(i) for i in range(3)] c = random.uniform(0, 0.05) i = random.randint(0, 2) - use_eth = i == 2 - amounts = [0, 0, 0] - value = 0 - for j in range(3): - if i == j: - amounts[i] = int(balances[i] * c) - if use_eth: - value = amounts[i] - - swap.add_liquidity(amounts, 0, use_eth, value=value) - - boa.env.time_travel(random.randint(12, 600)) - - -def _random_deposit_eth(swap): - balances = [swap.balances(i) for i in range(3)] - c = random.uniform(0, 0.05) - i = 2 - use_eth = True amounts = [0, 0, 0] - value = 0 for j in range(3): if i == j: amounts[i] = int(balances[i] * c) - if use_eth: - value = amounts[i] - swap.add_liquidity(amounts, 0, use_eth, value=value) + swap.add_liquidity(amounts, 0) boa.env.time_travel(random.randint(12, 600)) @@ -88,7 +61,7 @@ def _random_proportional_withdraw(swap): amount = int(swap.totalSupply() * random.uniform(0, 0.01)) - swap.remove_liquidity(amount, [0, 0, 0], True) + swap.remove_liquidity(amount, [0, 0, 0]) boa.env.time_travel(random.randint(12, 600)) @@ -97,13 +70,11 @@ def _random_withdraw_one(swap): i = random.randint(0, 2) amount = int(swap.totalSupply() * 0.01) - use_eth = i == 2 - - swap.remove_liquidity_one_coin(amount, i, 0, use_eth) + swap.remove_liquidity_one_coin(amount, i, 0) @pytest.mark.profile -def test_profile_amms(swap_with_deposit, coins, user, math_contract): +def test_profile_amms(swap_with_deposit, coins, user): swap = swap_with_deposit @@ -126,9 +97,6 @@ def test_profile_amms(swap_with_deposit, coins, user, math_contract): # deposit single token: _random_deposit_one(swap) - # deposit only eth: - _random_deposit_eth(swap) - # swap: _random_exchange(swap) diff --git a/tests/boa/unitary/factory/test_deploy_pool.py b/tests/boa/unitary/factory/test_deploy_pool.py index c6486057..909fa580 100644 --- a/tests/boa/unitary/factory/test_deploy_pool.py +++ b/tests/boa/unitary/factory/test_deploy_pool.py @@ -18,9 +18,7 @@ def empty_factory(deployer, fee_receiver, owner, weth): def test_check_packed_params_on_deployment(swap, params, coins): # check packed precisions - unpacked_precisions = swap.internal._unpack( - swap._storage.packed_precisions.get() - ) + unpacked_precisions = swap.precisions() for i in range(len(coins)): assert unpacked_precisions[i] == 10 ** (18 - coins[i].decimals()) diff --git a/tests/boa/unitary/math/test_get_p.py b/tests/boa/unitary/math/test_get_p.py index 85eb231e..04c676a0 100644 --- a/tests/boa/unitary/math/test_get_p.py +++ b/tests/boa/unitary/math/test_get_p.py @@ -52,12 +52,10 @@ def get_p( def _get_dydx_vyper(swap, i, j, price_calc): - # ANN = swap.A() - # A = ANN // 10**4 // 3**3 - A = swap.A() - gamma = swap.gamma() - - xp = swap.internal.xp() + xp = swap.internal.xp( + swap._storage.balances.get(), + swap._storage.price_scale_packed.get(), + ) for k in range(3): if k != i and k != j: @@ -67,10 +65,7 @@ def _get_dydx_vyper(swap, i, j, price_calc): x2 = xp[j] x3 = xp[k] - D = swap.D() - - dxdy = price_calc.get_p(x1, x2, x3, D, A, gamma) - return dxdy + return price_calc.get_p(x1, x2, x3, swap.D(), swap.A(), swap.gamma()) def _get_prices_vyper(swap, price_calc): diff --git a/tests/boa/unitary/math/test_get_p_expt.py b/tests/boa/unitary/math/test_get_p_expt.py index f1beb411..e55587e8 100644 --- a/tests/boa/unitary/math/test_get_p_expt.py +++ b/tests/boa/unitary/math/test_get_p_expt.py @@ -155,7 +155,12 @@ def _get_prices_vyper(swap, price_calc): A = swap.A() gamma = swap.gamma() - xp = swap.internal.xp() + balances = [] + for i in range(3): + balances.append(swap.balances(i)) + + xp = swap.internal.xp(balances, swap._storage.price_scale_packed.get()) + D = swap.D() p = price_calc.get_p(xp, D, [A, gamma]) @@ -224,11 +229,12 @@ def test_against_expt(dydx_optimised_math): def _imbalance_swap(swap, coins, imbalance_frac, user, dollar_amount, i, j): # make swap imbalanced: - mint_for_testing(coins[0], user, int(swap.balances(0) * imbalance_frac)) + imbalance_amount = int(swap.balances(i) * imbalance_frac) + mint_for_testing(coins[i], user, imbalance_amount) try: with boa.env.prank(user): - swap.exchange(i, j, coins[0].balanceOf(user), 0) + swap.exchange(i, j, imbalance_amount, 0) except boa.BoaError as b_error: assert_string_contains( b_error.stack_trace.last_frame.pretty_vm_reason, diff --git a/tests/boa/unitary/pool/admin/test_commit_params.py b/tests/boa/unitary/pool/admin/test_commit_params.py index 4d19b298..7632220b 100644 --- a/tests/boa/unitary/pool/admin/test_commit_params.py +++ b/tests/boa/unitary/pool/admin/test_commit_params.py @@ -3,17 +3,16 @@ import boa -def _commit_apply_new_params(swap, params): - swap.commit_new_parameters( +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"], ) - boa.env.time_travel(7 * 24 * 60 * 60) - swap.apply_new_parameters() def test_commit_accept_mid_fee(swap, factory_admin, params): @@ -21,7 +20,7 @@ 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): - _commit_apply_new_params(swap, p) + _apply_new_params(swap, p) mid_fee = swap.internal._unpack(swap._storage.packed_fee_params.get())[0] assert mid_fee == p["mid_fee"] @@ -32,7 +31,7 @@ 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): - _commit_apply_new_params(swap, p) + _apply_new_params(swap, p) out_fee = swap.internal._unpack(swap._storage.packed_fee_params.get())[1] assert out_fee == p["out_fee"] @@ -43,7 +42,7 @@ 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): - _commit_apply_new_params(swap, p) + _apply_new_params(swap, p) fee_gamma = swap.internal._unpack(swap._storage.packed_fee_params.get())[2] assert fee_gamma == p["fee_gamma"] @@ -57,7 +56,7 @@ def test_commit_accept_fee_params(swap, factory_admin, params): p["fee_gamma"] = 10**17 with boa.env.prank(factory_admin): - _commit_apply_new_params(swap, p) + _apply_new_params(swap, p) fee_params = swap.internal._unpack(swap._storage.packed_fee_params.get()) assert fee_params[0] == p["mid_fee"] @@ -70,7 +69,7 @@ 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): - _commit_apply_new_params(swap, p) + _apply_new_params(swap, p) allowed_extra_profit = swap.internal._unpack( swap._storage.packed_rebalancing_params.get() @@ -83,7 +82,7 @@ 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): - _commit_apply_new_params(swap, p) + _apply_new_params(swap, p) adjustment_step = swap.internal._unpack( swap._storage.packed_rebalancing_params.get() @@ -96,7 +95,7 @@ def test_commit_accept_ma_time(swap, factory_admin, params): p = copy.deepcopy(params) p["ma_time"] = 872 with boa.env.prank(factory_admin): - _commit_apply_new_params(swap, p) + _apply_new_params(swap, p) ma_time = swap.internal._unpack( swap._storage.packed_rebalancing_params.get() @@ -104,6 +103,16 @@ def test_commit_accept_ma_time(swap, factory_admin, params): 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) @@ -112,7 +121,7 @@ def test_commit_accept_rebalancing_params(swap, factory_admin, params): p["ma_time"] = 1000 with boa.env.prank(factory_admin): - _commit_apply_new_params(swap, p) + _apply_new_params(swap, p) rebalancing_params = swap.internal._unpack( swap._storage.packed_rebalancing_params.get() diff --git a/tests/boa/unitary/pool/admin/test_revert_commit_params.py b/tests/boa/unitary/pool/admin/test_revert_commit_params.py index ff2c9592..48228166 100644 --- a/tests/boa/unitary/pool/admin/test_revert_commit_params.py +++ b/tests/boa/unitary/pool/admin/test_revert_commit_params.py @@ -3,14 +3,15 @@ import boa -def _commit_new_params(swap, params): - swap.commit_new_parameters( +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"], ) @@ -20,16 +21,16 @@ def test_commit_incorrect_fee_params(swap, factory_admin, params): p["mid_fee"] = p["out_fee"] + 1 with boa.env.prank(factory_admin): with boa.reverts("mid-fee is too high"): - _commit_new_params(swap, p) + _apply_new_params(swap, p) p["out_fee"] = 0 with boa.reverts("fee is out of range"): - _commit_new_params(swap, p) + _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 - _commit_new_params(swap, p) + _apply_new_params(swap, p) logs = swap.get_logs()[0] assert logs.args[1] == params["out_fee"] @@ -42,10 +43,10 @@ def test_commit_incorrect_fee_gamma(swap, factory_admin, params): with boa.env.prank(factory_admin): with boa.reverts("fee_gamma out of range [1 .. 10**18]"): - _commit_new_params(swap, p) + _apply_new_params(swap, p) p["fee_gamma"] = 10**18 + 1 - _commit_new_params(swap, p) + _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"] @@ -61,7 +62,7 @@ def test_commit_rebalancing_params(swap, factory_admin, params): with boa.env.prank(factory_admin): with boa.env.anchor(): - _commit_new_params(swap, p) + _apply_new_params(swap, p) logs = swap.get_logs()[0] # values revert to contract's storage values: @@ -71,48 +72,10 @@ def test_commit_rebalancing_params(swap, factory_admin, params): with boa.reverts("MA time should be longer than 60/ln(2)"): p["ma_time"] = 86 - _commit_new_params(swap, p) - - -def test_revert_commit_twice(swap, factory_admin, params): - - with boa.env.prank(factory_admin): - _commit_new_params(swap, params) - - with boa.reverts(dev="active action"): - _commit_new_params(swap, params) + _apply_new_params(swap, p) def test_revert_unauthorised_commit(swap, user, params): with boa.env.prank(user), boa.reverts(dev="only owner"): - _commit_new_params(swap, params) - - -def test_unauthorised_revert(swap, user, factory_admin, params): - - with boa.env.prank(factory_admin): - _commit_new_params(swap, params) - - with boa.env.prank(user), boa.reverts(dev="only owner"): - swap.revert_new_parameters() - - -def test_revert_new_params(swap, factory_admin, params): - - p = copy.deepcopy(params) - p["mid_fee"] += 1 - - with boa.env.prank(factory_admin): - _commit_new_params(swap, p) - - mid_fee = swap.internal._unpack(swap._storage.packed_fee_params.get())[ - 0 - ] - assert params["mid_fee"] == mid_fee - - swap.revert_new_parameters() - boa.env.time_travel(7 * 24 * 60 * 60) - - with boa.reverts(dev="no active action"): - swap.apply_new_parameters() + _apply_new_params(swap, params) diff --git a/tests/boa/unitary/pool/stateful/stateful_base.py b/tests/boa/unitary/pool/stateful/stateful_base.py index 50a4a37a..f47a770c 100644 --- a/tests/boa/unitary/pool/stateful/stateful_base.py +++ b/tests/boa/unitary/pool/stateful/stateful_base.py @@ -40,6 +40,7 @@ def __init__(self): self.user_balances = {u: [0] * 3 for u in self.accounts} self.balances = self.initial_deposit[:] self.xcp_profit = 10**18 + self.xcp_profit_a = 10**18 self.total_supply = 0 self.previous_pool_profit = 0 @@ -63,8 +64,6 @@ def setup(self, user_id=0): self.total_supply = self.token.balanceOf(user) def get_coin_balance(self, user, coin): - if coin.symbol() == "WETH": - return boa.env.get_balance(user) return coin.balanceOf(user) def convert_amounts(self, amounts): @@ -163,7 +162,7 @@ def _exchange( with boa.env.prank(user): self.coins[exchange_i].approve(self.swap, 2**256 - 1) out = self.swap.exchange( - exchange_i, exchange_j, exchange_amount_in, 0 + exchange_i, exchange_j, exchange_amount_in, calc_amount ) except Exception: @@ -179,7 +178,6 @@ def _exchange( and self.check_limits(_amounts) ): raise - return None # This is to check that we didn't end up in a borked state after @@ -220,10 +218,7 @@ def sleep(self, sleep_time): def balances(self): balances = [self.swap.balances(i) for i in range(3)] - eth_balance_amm = boa.env.get_balance(self.swap.address) - balances_of = [c.balanceOf(self.swap) for c in self.coins] - balances_of[2] = eth_balance_amm # eth is set at i==2 for i in range(3): assert self.balances[i] == balances[i] == balances_of[i] @@ -265,11 +260,43 @@ def up_only_profit(self): @contextlib.contextmanager def upkeep_on_claim(self): - admin_balance = self.swap.balanceOf(self.fee_receiver) + 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: - _claimed = self.swap.balanceOf(self.fee_receiver) - admin_balance - if _claimed > 0: - self.total_supply += _claimed - self.xcp_profit = self.swap.xcp_profit() + + 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(3): + 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/boa/unitary/pool/stateful/test_admin_fee_claim.py b/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py index 96be6c32..8064e056 100644 --- a/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py +++ b/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py @@ -3,7 +3,8 @@ from boa.test import strategy from hypothesis.stateful import rule, run_state_machine_as_test -from tests.boa.unitary.pool.stateful.stateful_base import StatefulBase +# from tests.boa.unitary.pool.stateful.stateful_base import StatefulBase +from tests.boa.unitary.pool.stateful.test_stateful import ProfitableState MAX_SAMPLES = 20 STEP_COUNT = 100 @@ -14,10 +15,16 @@ def approx(x1, x2, precision): return abs(log(x1 / x2)) <= precision -class StatefulAdmin(StatefulBase): +class StatefulAdmin(ProfitableState): exchange_amount_in = strategy( "uint256", min_value=10**17, max_value=10**5 * 10**18 ) + deposit_amounts = strategy( + "uint256[3]", min_value=10**18, max_value=10**9 * 10**18 + ) + token_amount = strategy( + "uint256", min_value=10**18, max_value=10**12 * 10**18 + ) exchange_i = strategy("uint8", max_value=2) exchange_j = strategy("uint8", max_value=2) user = strategy("address") @@ -52,11 +59,24 @@ def exchange(self, exchange_amount_in, exchange_i, exchange_j, user): exchange_amount_in_converted, exchange_i, exchange_j, user ) - @rule() - def claim_admin_fees(self): + @rule(deposit_amounts=deposit_amounts, user=user) + def deposit(self, deposit_amounts, user): + deposit_amounts[1:] = [deposit_amounts[0]] + [ + deposit_amounts[i] * 10**18 // self.swap.price_oracle(i - 1) + for i in [1, 2] + ] + super().deposit(deposit_amounts, user) + + @rule( + token_amount=token_amount, + exchange_i=exchange_i, + user=user, + ) + def remove_liquidity_one_coin(self, token_amount, exchange_i, user): - with self.upkeep_on_claim(): - self.swap.claim_admin_fees() + super().remove_liquidity_one_coin( + token_amount, exchange_i, user, False + ) def test_admin_fee(swap, views_contract, users, pool_coins, tricrypto_factory): diff --git a/tests/boa/unitary/pool/stateful/test_gas_realistic.py b/tests/boa/unitary/pool/stateful/test_gas_realistic.py index 61534e6e..aaea15ac 100644 --- a/tests/boa/unitary/pool/stateful/test_gas_realistic.py +++ b/tests/boa/unitary/pool/stateful/test_gas_realistic.py @@ -56,9 +56,12 @@ def deposit(self, deposit_amount, exchange_i, user): amounts[exchange_i] = deposit_amount - new_balances = [x + y for x, y in zip(self.balances, amounts)] mint_for_testing(self.coins[exchange_i], user, deposit_amount) + with boa.env.prank(user): + for coin in self.coins: + coin.approve(self.swap, 2**256 - 1) + try: tokens = self.token.balanceOf(user) @@ -68,7 +71,9 @@ def deposit(self, deposit_amount, exchange_i, user): tokens = self.token.balanceOf(user) - tokens self.total_supply += tokens - self.balances = new_balances + + for i in range(3): + self.balances[i] += amounts[i] except Exception: @@ -85,10 +90,6 @@ def remove_liquidity_one_coin( self, token_fraction, exchange_i, user, update_D ): - if update_D: - with self.upkeep_on_claim(): - self.swap.claim_admin_fees() - token_amount = token_fraction * self.total_supply // 10**18 d_token = self.token.balanceOf(user) if token_amount == 0 or token_amount > d_token: diff --git a/tests/boa/unitary/pool/stateful/test_ramp.py b/tests/boa/unitary/pool/stateful/test_ramp.py index 20831eb8..03ef8120 100644 --- a/tests/boa/unitary/pool/stateful/test_ramp.py +++ b/tests/boa/unitary/pool/stateful/test_ramp.py @@ -35,6 +35,8 @@ def setup(self, user_id=0): with boa.env.prank(self.tricrypto_factory.admin()): self.swap.ramp_A_gamma(new_A, new_gamma, block_time + 14 * 86400) + self.xcp_profit_a_init = self.swap.xcp_profit_a() + @rule(deposit_amounts=deposit_amounts, user=user) def deposit(self, deposit_amounts, user): deposit_amounts[1:] = [deposit_amounts[0]] + [ @@ -58,9 +60,6 @@ def exchange( user, check_out_amount, ): - if check_out_amount: - with self.upkeep_on_claim(): - self.swap.claim_admin_fees() if exchange_i > 0: exchange_amount_in = ( @@ -89,10 +88,6 @@ def remove_liquidity_one_coin( self, token_amount, exchange_i, user, check_out_amount ): if check_out_amount: - - with self.upkeep_on_claim(): - self.swap.claim_admin_fees() - super().remove_liquidity_one_coin( token_amount, exchange_i, user, ALLOWED_DIFFERENCE ) @@ -106,6 +101,10 @@ def virtual_price(self): # Invariant is not conserved here pass + @invariant() + def check_xcp_profit_a_doesnt_increase(self): + assert self.swap.xcp_profit_a() == self.xcp_profit_a_init + def test_ramp(swap, views_contract, users, pool_coins, tricrypto_factory): from hypothesis import settings diff --git a/tests/boa/unitary/pool/stateful/test_ramp_nocheck.py b/tests/boa/unitary/pool/stateful/test_ramp_nocheck.py index 63abadc9..4ac789fc 100644 --- a/tests/boa/unitary/pool/stateful/test_ramp_nocheck.py +++ b/tests/boa/unitary/pool/stateful/test_ramp_nocheck.py @@ -46,6 +46,8 @@ def initialize(self, future_A, future_gamma): boa.env.vm.state.timestamp + 14 * 86400, ) + self.xcp_profit_a_init = self.swap.xcp_profit_a() + @rule( exchange_amount_in=exchange_amount_in, exchange_i=exchange_i, @@ -75,6 +77,10 @@ def up_only_profit(self): # so we need to override super().up_only_profit() pass + @invariant() + def check_xcp_profit_a_doesnt_increase(self): + assert self.swap.xcp_profit_a() == self.xcp_profit_a_init + def test_ramp(swap, views_contract, users, pool_coins, tricrypto_factory): from hypothesis import settings diff --git a/tests/boa/unitary/pool/stateful/test_simulate.py b/tests/boa/unitary/pool/stateful/test_simulate.py index a66d2338..951752c5 100644 --- a/tests/boa/unitary/pool/stateful/test_simulate.py +++ b/tests/boa/unitary/pool/stateful/test_simulate.py @@ -97,7 +97,6 @@ def exchange(self, exchange_amount_in, exchange_i, exchange_j, user): if self.swap_out: dy_trader = self.trader.buy(dx, exchange_i, exchange_j) - self.trader.tweak_price(boa.env.vm.state.timestamp) # check if output value from exchange is similar: diff --git a/tests/boa/unitary/pool/stateful/test_stateful.py b/tests/boa/unitary/pool/stateful/test_stateful.py index 81a1c19f..acfb5453 100644 --- a/tests/boa/unitary/pool/stateful/test_stateful.py +++ b/tests/boa/unitary/pool/stateful/test_stateful.py @@ -30,19 +30,23 @@ def deposit(self, deposit_amounts, user): return amounts = self.convert_amounts(deposit_amounts) - 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.token.balanceOf(user) with boa.env.prank(user), self.upkeep_on_claim(): self.swap.add_liquidity(amounts, 0) tokens = self.token.balanceOf(user) - tokens self.total_supply += tokens - self.balances = new_balances + + for i in range(3): + self.balances[i] += amounts[i] + except Exception: + if self.check_limits(amounts): raise else: @@ -103,7 +107,7 @@ def remove_liquidity(self, token_amount, user): else: amounts = [self.get_coin_balance(user, c) for c in self.coins] tokens = self.token.balanceOf(user) - with boa.env.prank(user), self.upkeep_on_claim(): + with boa.env.prank(user): self.swap.remove_liquidity(token_amount, [0] * 3) tokens -= self.token.balanceOf(user) self.total_supply -= tokens @@ -127,10 +131,6 @@ def remove_liquidity(self, token_amount, user): def remove_liquidity_one_coin( self, token_amount, exchange_i, user, check_out_amount ): - if check_out_amount: - with self.upkeep_on_claim(): - self.swap.claim_admin_fees() - try: calc_out_amount = self.swap.calc_withdraw_one_coin( token_amount, exchange_i diff --git a/tests/boa/unitary/pool/test_callback.py b/tests/boa/unitary/pool/test_callback.py deleted file mode 100644 index 3d841897..00000000 --- a/tests/boa/unitary/pool/test_callback.py +++ /dev/null @@ -1,150 +0,0 @@ -import boa -import pytest -from boa.test import strategy -from hypothesis import given, settings - -from tests.boa.utils.tokens import mint_for_testing - - -@pytest.fixture(scope="module") -def callbacker(): - return boa.env.generate_address() - - -@pytest.fixture(scope="module", autouse=True) -def zap(swap_with_deposit, coins, callbacker): - - with boa.env.prank(callbacker): - _zap = boa.load( - "contracts/mocks/CallbackTestZap.vy", swap_with_deposit.address - ) - for coin in coins: - coin.approve(_zap.address, 2**256 - 1) - - return _zap - - -@given( - dx=strategy("uint256", min_value=10**10, max_value=100 * 10**18), - i=strategy("uint8", min_value=0, max_value=2), - j=strategy("uint8", min_value=0, max_value=2), -) -@settings(deadline=None) -def test_revert_good_callback_not_enough_coins(zap, callbacker, i, j, dx): - if i == j: - return - - with boa.env.prank(callbacker), boa.reverts(): - zap.good_exchange(i, j, dx, 0) - - -@given( - dx=strategy("uint256", min_value=10**10, max_value=100 * 10**18), - j=strategy("uint8", min_value=0, max_value=1), -) -@settings(deadline=None) -def test_revert_good_callback_input_eth(zap, callbacker, coins, j, dx): - - mint_for_testing(coins[2], callbacker, dx, True) - - with boa.env.prank(callbacker), boa.reverts(): - zap.good_exchange(2, j, dx, 0, True, value=dx) - - -@given( - dx=strategy("uint256", min_value=10**10, max_value=100 * 10**18), - i=strategy("uint8", min_value=0, max_value=1), -) -@settings(deadline=None) -def test_success_good_callback_output_eth( - swap_with_deposit, views_contract, zap, callbacker, coins, i, dx -): - - mint_for_testing(coins[i], callbacker, dx) - - dy = views_contract.get_dy(i, 2, dx, swap_with_deposit) - - bal_before = boa.env.get_balance(callbacker) - bal_weth_before = coins[2].balanceOf(callbacker) - bal_in_before = coins[i].balanceOf(callbacker) - - with boa.env.prank(callbacker): - out = zap.good_exchange(i, 2, dx, 0, True) - - assert out == dy - assert boa.env.get_balance(callbacker) == bal_before + dy - assert coins[2].balanceOf(callbacker) == bal_weth_before - assert coins[i].balanceOf(callbacker) == bal_in_before - dx - - -@given( - dx=strategy("uint256", min_value=10**10, max_value=100 * 10**18), - i=strategy("uint8", min_value=0, max_value=2), - j=strategy("uint8", min_value=0, max_value=2), -) -@settings(deadline=None) -def test_good_callback_erc20( - swap_with_deposit, views_contract, zap, callbacker, coins, i, j, dx -): - if i == j: - return - - dy = views_contract.get_dy(i, j, dx, swap_with_deposit) - - mint_for_testing(coins[i], callbacker, dx, False) - - with boa.env.prank(callbacker): - zap.good_exchange(i, j, dx, 0, False) - - assert zap.input_amount() == dx - assert zap.output_amount() == dy - - -@given( - dx=strategy("uint256", min_value=10**10, max_value=100 * 10**18), - i=strategy("uint8", min_value=0, max_value=1), -) -@settings(deadline=None) -def test_good_callback_output_eth( - swap_with_deposit, views_contract, zap, callbacker, coins, i, dx -): - - dy = views_contract.get_dy(i, 2, dx, swap_with_deposit) - - mint_for_testing(coins[i], callbacker, dx) - - with boa.env.prank(callbacker): - zap.good_exchange(i, 2, dx, 0, True) - - assert zap.input_amount() == dx - assert zap.output_amount() == dy - - -@given( - amount=strategy("uint256", min_value=10**10, max_value=100 * 10**18), - i=strategy("uint8", min_value=0, max_value=2), - j=strategy("uint8", min_value=0, max_value=2), -) -@settings(deadline=None) -def test_evil_callback_erc20(zap, coins, i, j, callbacker, amount): - - if i == j: - return - - mint_for_testing(coins[i], callbacker, amount * 2, False) - - # set dx in callback sig to half of amount: - # callback sends 2x what it exchanges. - with boa.env.prank(callbacker): - zap.set_evil_input_amount(amount * 2) - - with boa.reverts(): - zap.evil_exchange(i, j, amount, 0, False) - - # set dx in callback sig to twice the amount: - # callback sends pool half of what it exchanges. - with boa.env.prank(callbacker): - zap.set_evil_input_amount(amount // 2) - - with boa.reverts(): - zap.evil_exchange(i, j, amount, 0, False) diff --git a/tests/boa/unitary/pool/test_deposit_withdraw.py b/tests/boa/unitary/pool/test_deposit_withdraw.py index 8ad4a3ae..cfb94d18 100644 --- a/tests/boa/unitary/pool/test_deposit_withdraw.py +++ b/tests/boa/unitary/pool/test_deposit_withdraw.py @@ -34,8 +34,8 @@ def test_1st_deposit_and_last_withdraw(swap, coins, user, fee_receiver): with boa.env.prank(user): swap.add_liquidity(quantities, 0) - # test if eth was deposited: - assert boa.env.get_balance(swap.address) == bal_before + quantities[2] + # test if eth wasnt deposited: + assert boa.env.get_balance(swap.address) == bal_before token_balance = swap.balanceOf(user) assert ( @@ -79,8 +79,8 @@ def test_first_deposit_full_withdraw_second_deposit( assert swap.xcp_profit() >= 10**18 assert swap.virtual_price() >= 10**18 - # test if eth was deposited: - assert boa.env.get_balance(swap.address) == quantities[2] + eth_bal_before + # 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] @@ -114,56 +114,6 @@ def test_second_deposit_single_token( with boa.env.prank(user): swap_with_deposit.add_liquidity(quantities, 0) - # deposit single sided but pure eth: - if i == 2: - mint_for_testing(coins[2], user, amount, True) - with boa.env.prank(user): - swap_with_deposit.add_liquidity( - quantities, 0, True, value=quantities[2] - ) - - -def test_claim_admin_fees_post_emptying_and_depositing( - test_first_deposit_full_withdraw_second_deposit, user, coins -): - - swap = test_first_deposit_full_withdraw_second_deposit - admin_balance_before = swap.balanceOf(swap.fee_receiver()) - assert admin_balance_before >= 0 - - # do another deposit to have some fees for the admin: - quantities = [10**5 * 10**36 // p for p in INITIAL_PRICES] - for coin, q in zip(coins, quantities): - mint_for_testing(coin, user, q) - with boa.env.prank(user): - coin.approve(swap, 2**256 - 1) - - # Accumulate fees - with boa.env.prank(user): - - # Add some liquidity: - swap.add_liquidity(quantities, 0) - - assert swap.totalSupply() > 0 - - # Some swaps here and there: - swap.exchange(0, 1, coins[0].balanceOf(user), 0) - swap.exchange(1, 0, coins[1].balanceOf(user), 0) - - assert swap.xcp_profit() > 0 - assert swap.virtual_price() > 10**18 - - if swap.totalSupply() > 10**18: - assert swap.xcp_profit_a() > 10**18 - else: - assert swap.xcp_profit_a() == 10**18 - - with boa.env.prank(user): - swap.claim_admin_fees() - - admin_balance_after = swap.balanceOf(swap.fee_receiver()) - assert admin_balance_after > admin_balance_before - @given( values=strategy( @@ -204,12 +154,19 @@ def test_second_deposit( 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(3)] + claimed_fees = [0, 0, 0] 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] for i in range(3) + swap_with_deposit.balances(i) - d_balances[i] + claimed_fees[i] + for i in range(3) ] measured = swap_with_deposit.balanceOf(user) - measured @@ -254,12 +211,19 @@ def test_second_deposit_one( ) measured = swap_with_deposit.balanceOf(user) d_balances = [swap_with_deposit.balances(i) for i in range(3)] + claimed_fees = [0, 0, 0] 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] for i in range(3) + swap_with_deposit.balances(i) - d_balances[i] + claimed_fees[i] + for i in range(3) ] measured = swap_with_deposit.balanceOf(user) - measured @@ -365,12 +329,18 @@ def test_immediate_withdraw_one( measured = coins[i].balanceOf(user) d_balances = [swap_with_deposit.balances(k) for k in range(3)] + claimed_fees = [0, 0, 0] 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 @@ -394,50 +364,13 @@ def test_immediate_withdraw_one( assert approx(calculated, measured, 1e-3) for k in range(3): + claimed_tokens = claimed_fees[k] if k == i: - assert d_balances[k] == measured + assert d_balances[k] == measured + claimed_tokens else: - assert d_balances[k] == 0 + 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) views_contract.get_dy(0, 2, 10**16, swap_with_deposit) - - -def test_claim_fees_before_second_deposit( - swap_multiprecision, tricrypto_coins, user, deployer -): - for coin in tricrypto_coins: - for acc in [deployer, user]: - with boa.env.prank(acc): - coin.approve(swap_multiprecision, 2**256 - 1) - - # mint lp tokens for first depositor and remove all but 1 wei of lp tokens - deployer_mint = [11000 * 10**6, 51 * 10**6, 71 * 10**17] - for coin, q_d in zip(tricrypto_coins, deployer_mint): - mint_for_testing(coin, deployer, q_d) - - with boa.env.prank(deployer): - lp_tokens_minted = swap_multiprecision.add_liquidity( - [2100 * 10**6, 1 * 10**7, 14 * 10**17], 0 - ) - swap_multiprecision.remove_liquidity(lp_tokens_minted - 1, [0, 0, 0]) - - # deployer sends funds to pool and claims admin fees - with boa.env.prank(deployer): - for coin in tricrypto_coins: - coin.transfer(swap_multiprecision, coin.balanceOf(deployer)) - swap_multiprecision.claim_admin_fees() - - # user deposits: - user_mint = [20000 * 10**6, 1 * 10**8, 14 * 10**18] - for coin, q_u in zip(tricrypto_coins, user_mint): - mint_for_testing(coin, user, q_u) - - with boa.env.prank(user): - user_lp_token_minted = swap_multiprecision.add_liquidity( - [c.balanceOf(user) for c in tricrypto_coins], 0 - ) - - assert user_lp_token_minted > 0 diff --git a/tests/boa/unitary/pool/test_exchange.py b/tests/boa/unitary/pool/test_exchange.py index 77d729e2..f7906aa5 100644 --- a/tests/boa/unitary/pool/test_exchange.py +++ b/tests/boa/unitary/pool/test_exchange.py @@ -1,5 +1,4 @@ import boa -import pytest from boa.test import strategy from hypothesis import given, settings # noqa @@ -28,6 +27,7 @@ def test_exchange_all( ): if i == j or i > 2 or j > 2: + with boa.reverts(): views_contract.get_dy(i, j, 10**6, swap_with_deposit) @@ -35,6 +35,7 @@ def test_exchange_all( swap_with_deposit.exchange(i, j, 10**6, 0) else: + amount = amount * 10**18 // INITIAL_PRICES[i] mint_for_testing(coins[i], user, amount) @@ -60,39 +61,47 @@ def test_exchange_all( assert -d_balance_j == measured_j -@pytest.mark.parametrize("j", [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("uint", min_value=0, max_value=3), + j=strategy("uint", min_value=0, max_value=3), ) @settings(**SETTINGS) -def test_exchange_from_eth( +def test_exchange_received_success( swap_with_deposit, views_contract, coins, user, amount, + i, j, ): - amount = amount * 10**18 // INITIAL_PRICES[2] + if i == j or i > 2 or j > 2: + + return + + amount = amount * 10**18 // INITIAL_PRICES[i] + mint_for_testing(coins[i], user, amount) - calculated = views_contract.get_dy(2, j, amount, swap_with_deposit) + calculated = views_contract.get_dy(i, j, amount, swap_with_deposit) - measured_i = boa.env.get_balance(user) + measured_i = coins[i].balanceOf(user) measured_j = coins[j].balanceOf(user) - d_balance_i = swap_with_deposit.balances(2) + 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( - 2, j, amount, int(0.999 * calculated), True, value=amount + coins[i].transfer(swap_with_deposit, amount) + swap_with_deposit.exchange_received( + i, j, amount, int(0.999 * calculated), user ) - measured_i -= boa.env.get_balance(user) + measured_i -= coins[i].balanceOf(user) measured_j = coins[j].balanceOf(user) - measured_j - d_balance_i = swap_with_deposit.balances(2) - d_balance_i + 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 @@ -102,66 +111,34 @@ def test_exchange_from_eth( assert -d_balance_j == measured_j -@pytest.mark.parametrize("i", [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("uint", min_value=0, max_value=3), + j=strategy("uint", min_value=0, max_value=3), ) @settings(**SETTINGS) -def test_exchange_into_eth( +def test_exchange_received_revert_on_no_transfer( swap_with_deposit, views_contract, coins, user, amount, i, + j, ): - amount = amount * 10**18 // INITIAL_PRICES[i] - mint_for_testing(coins[i], user, amount) - - calculated = views_contract.get_dy(i, 2, amount, swap_with_deposit) - - measured_i = coins[i].balanceOf(user) - measured_j = boa.env.get_balance(user) - d_balance_i = swap_with_deposit.balances(i) - d_balance_j = swap_with_deposit.balances(2) - - with boa.env.prank(user): - swap_with_deposit.exchange(i, 2, amount, int(0.999 * calculated), True) - - measured_i -= coins[i].balanceOf(user) - measured_j = boa.env.get_balance(user) - measured_j - d_balance_i = swap_with_deposit.balances(i) - d_balance_i - d_balance_j = swap_with_deposit.balances(2) - d_balance_j + if i == j or i > 2 or j > 2: - assert amount == measured_i - assert calculated == measured_j + return - assert d_balance_i == amount - assert -d_balance_j == measured_j + 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) -@pytest.mark.parametrize("j", [0, 1]) -@pytest.mark.parametrize("modifier", [0, 1.01, 2]) -def test_incorrect_eth_amount(swap_with_deposit, user, j, modifier): - amount = 10**18 - with boa.reverts(dev="incorrect eth amount"), boa.env.prank(user): - swap_with_deposit.exchange( - 2, j, amount, 0, True, value=int(amount * modifier) + 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 ) - - -@pytest.mark.parametrize("j", [0, 1]) -def test_send_eth_without_use_eth(swap_with_deposit, user, j): - amount = 10**18 - with boa.reverts(dev="nonzero eth amount"), boa.env.prank(user): - swap_with_deposit.exchange(2, j, amount, 0, False, value=amount) - - -@pytest.mark.parametrize("i", [0, 1]) -def test_send_eth_with_incorrect_i(swap_with_deposit, user, i): - amount = 10**18 - with boa.reverts(dev="nonzero eth amount"), boa.env.prank(user): - swap_with_deposit.exchange(i, 2, amount, 0, True, value=amount) diff --git a/tests/boa/unitary/pool/test_oracles.py b/tests/boa/unitary/pool/test_oracles.py index 89836d9c..2a7bbbcf 100644 --- a/tests/boa/unitary/pool/test_oracles.py +++ b/tests/boa/unitary/pool/test_oracles.py @@ -95,6 +95,65 @@ def test_ma(swap_with_deposit, coins, user, amount, i, j, t): assert abs(log2(theory / p3)) < 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=2), + j=strategy("uint8", min_value=0, max_value=2), + t=strategy("uint256", min_value=10, max_value=10 * 86400), +) +@settings(**SETTINGS) +def test_xcp_ma( + swap_with_deposit, math_contract, coins, user, amount, i, j, t +): + + if i == j: + return + + price_scale = [swap_with_deposit.price_scale(i) for i in range(2)] + D0 = swap_with_deposit.D() + xp = [0, 0, 0] + xp[0] = D0 // 3 # N_COINS = 3 + for k in range(2): + xp[k + 1] = D0 * 10**18 // (3 * price_scale[k]) + + xcp0 = math_contract.geometric_mean(xp) + + # 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, j, 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( diff --git a/tests/boa/unitary/pool/test_use_eth.py b/tests/boa/unitary/pool/test_use_eth.py deleted file mode 100644 index 4b22914f..00000000 --- a/tests/boa/unitary/pool/test_use_eth.py +++ /dev/null @@ -1,215 +0,0 @@ -import boa -from boa.test import strategy -from hypothesis import given, settings - -from tests.boa.fixtures.pool import INITIAL_PRICES, _get_deposit_amounts -from tests.boa.utils.tokens import mint_for_testing - - -@given( - amount=strategy("uint256", min_value=10**10, max_value=10**18), - i=strategy("uint256", min_value=0, max_value=1), -) -@settings(deadline=None) -def test_exchange_eth_in(swap_with_deposit, amount, coins, user, i): - - assert coins[i].balanceOf(user) == 0 - swap_eth_balance = boa.env.get_balance(swap_with_deposit.address) - swap_token_balance = swap_with_deposit.balances(i) - - with boa.env.prank(user): - dy = swap_with_deposit.exchange(2, i, amount, 0, True, value=amount) - - assert coins[i].balanceOf(user) > 0 - assert ( - boa.env.get_balance(swap_with_deposit.address) - == amount + swap_eth_balance - ) - assert swap_with_deposit.balances(i) == swap_token_balance - dy - - -@given( - amount=strategy("uint256", min_value=10**10, max_value=10**18), - i=strategy("uint256", min_value=0, max_value=1), -) -@settings(deadline=None) -def test_exchange_underlying_eth_in(swap_with_deposit, amount, coins, user, i): - - assert coins[i].balanceOf(user) == 0 - swap_eth_balance = boa.env.get_balance(swap_with_deposit.address) - swap_token_balance = swap_with_deposit.balances(i) - - with boa.env.prank(user): - dy = swap_with_deposit.exchange_underlying( - 2, i, amount, 0, value=amount - ) - - assert coins[i].balanceOf(user) > 0 - assert ( - boa.env.get_balance(swap_with_deposit.address) - == amount + swap_eth_balance - ) - assert swap_with_deposit.balances(i) == swap_token_balance - dy - - -@given( - amount=strategy("uint256", min_value=10**10, max_value=10**18), - i=strategy("uint256", min_value=0, max_value=1), -) -@settings(deadline=None) -def test_exchange_eth_out(swap_with_deposit, amount, coins, user, i): - - old_balance = boa.env.get_balance(user) - swap_eth_balance = boa.env.get_balance(swap_with_deposit.address) - swap_token_balance = swap_with_deposit.balances(i) - - mint_for_testing(coins[i], user, amount) - - with boa.env.prank(user): - swap_with_deposit.exchange(i, 2, amount, 0, True) - - assert boa.env.get_balance(user) - old_balance > 0 - assert boa.env.get_balance( - user - ) - old_balance == swap_eth_balance - swap_with_deposit.balances(2) - assert swap_with_deposit.balances(i) - swap_token_balance == amount - - -@given( - amount=strategy("uint256", min_value=10**10, max_value=10**18), - i=strategy("uint256", min_value=0, max_value=1), -) -@settings(deadline=None) -def test_exchange_underlying_eth_out( - swap_with_deposit, amount, coins, user, i -): - - old_balance = boa.env.get_balance(user) - swap_eth_balance = boa.env.get_balance(swap_with_deposit.address) - swap_token_balance = swap_with_deposit.balances(i) - - mint_for_testing(coins[i], user, amount) - - with boa.env.prank(user): - swap_with_deposit.exchange_underlying(i, 2, amount, 0) - - assert boa.env.get_balance(user) - old_balance > 0 - assert boa.env.get_balance( - user - ) - old_balance == swap_eth_balance - swap_with_deposit.balances(2) - assert swap_with_deposit.balances(i) - swap_token_balance == amount - - -@given( - amount_usd=strategy("uint256", min_value=1, max_value=10**8), - use_eth=strategy("bool"), -) -@settings(deadline=None) -def test_add_liquidity_eth(swap, coins, user, amount_usd, use_eth): - - amounts = _get_deposit_amounts(amount_usd, INITIAL_PRICES, coins) - - for i in range(3): - if i == 2 and use_eth: - mint_for_testing(coins[i], user, amounts[i], True) - else: - mint_for_testing(coins[i], user, amounts[i]) - - initial_coin_balances = [c.balanceOf(user) for c in coins] - initial_eth_balance = boa.env.get_balance(user) - - with boa.env.prank(user): - for coin in coins: - coin.approve(swap, 2**256 - 1) - - if use_eth: - with boa.env.prank(user): - with boa.reverts(dev="incorrect eth amount"): - swap.add_liquidity(amounts, 0, True) - - swap.add_liquidity(amounts, 0, True, value=amounts[2]) - - assert coins[2].balanceOf(user) == initial_coin_balances[2] - assert initial_eth_balance - boa.env.get_balance(user) == amounts[2] - - else: - with boa.env.prank(user): - with boa.reverts(dev="nonzero eth amount"): - swap.add_liquidity(amounts, 0, False, value=amounts[2]) - - swap.add_liquidity(amounts, 0, False) - - assert ( - initial_coin_balances[2] - coins[2].balanceOf(user) == amounts[2] - ) - assert initial_eth_balance == boa.env.get_balance(user) - - for i in range(3): - if i == 2: - break - assert ( - initial_coin_balances[i] - coins[i].balanceOf(user) == amounts[i] - ) - - -@given( - frac=strategy("uint256", min_value=10**10, max_value=10**18), - use_eth=strategy("bool"), -) -@settings(deadline=None) -def test_remove_liquidity_eth(swap_with_deposit, coins, user, frac, use_eth): - - token_amount = swap_with_deposit.balanceOf(user) * frac // 10**18 - assert token_amount > 0 - - initial_coin_balances = [c.balanceOf(user) for c in coins] - initial_eth_balance = boa.env.get_balance(user) - - with boa.env.prank(user): - out = swap_with_deposit.remove_liquidity( - token_amount, [0, 0, 0], use_eth - ) - - if use_eth: - assert coins[2].balanceOf(user) == initial_coin_balances[2] - assert ( - abs(boa.env.get_balance(user) - (initial_eth_balance + out[2])) - == 0 - ) - else: - assert boa.env.get_balance(user) == initial_eth_balance - assert abs(coins[2].balanceOf(user) - out[2]) == 0 - - -@given( - frac=strategy("uint256", min_value=10**10, max_value=5 * 10**17), - i=strategy("uint8", min_value=0, max_value=1), - use_eth=strategy("bool"), -) -@settings(deadline=None) -def test_remove_liquidity_one_coin_eth( - swap_with_deposit, coins, user, frac, i, use_eth -): - - token_amount = swap_with_deposit.balanceOf(user) * frac // 10**18 - assert token_amount > 0 - - initial_coin_balances = [c.balanceOf(user) for c in coins] - initial_eth_balance = boa.env.get_balance(user) - - with boa.env.prank(user): - swap_with_deposit.remove_liquidity_one_coin( - token_amount, i, 0, use_eth - ) - - if i != 2 or not use_eth: - assert coins[i].balanceOf(user) > initial_coin_balances[i] - assert initial_eth_balance == boa.env.get_balance(user) - else: - assert boa.env.get_balance(user) > initial_eth_balance - assert coins[i].balanceOf(user) == initial_coin_balances[i] - - for j in range(3): - if i == j: - continue - assert coins[j].balanceOf(user) == initial_coin_balances[j] diff --git a/tests/boa/unitary/views/test_get_dx.py b/tests/boa/unitary/views/test_get_dx.py index ade6739c..24dde38c 100644 --- a/tests/boa/unitary/views/test_get_dx.py +++ b/tests/boa/unitary/views/test_get_dx.py @@ -18,6 +18,9 @@ def test_get_dx(i, j, amount_in, yuge_swap): if i == j: return + if amount_in == 0: + return + expected_out = yuge_swap.get_dy(i, j, amount_in) approx_in = yuge_swap.get_dx(i, j, expected_out)