From 0ad0764a84356fe9d67f370a1735f582ca8b7e93 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:29:01 +0200 Subject: [PATCH 01/53] feat: no more weth; no more public claim_admin_fees; wip: working on revamping claiming fees logic; --- contracts/main/CurveTricryptoOptimizedWETH.vy | 223 ++++-------------- 1 file changed, 51 insertions(+), 172 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index b29c68b6..d4ae4f06 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -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 @@ -300,24 +296,15 @@ 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, sender: address, receiver: address, - use_eth: bool ): """ @notice Transfers `_coin` from `sender` to `self` and calls `callback_sig` @@ -339,45 +326,34 @@ def _transfer_in( @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): + if callback_sig == empty(bytes32): - assert ERC20(_coin).transferFrom( - sender, self, dx, default_return_value=True - ) + assert ERC20(_coin).transferFrom( + sender, self, dx, default_return_value=True + ) - else: + 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) - ) + # --------- 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. - - if _coin == WETH20: - WETH(WETH20).withdraw(dx) # <--------- if WETH was transferred in - # previous step and `not use_eth`, withdraw WETH to ETH. + ) + 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. @internal -def _transfer_out( - _coin: address, _amount: uint256, use_eth: bool, receiver: address -): +def _transfer_out(_coin: address, _amount: uint256, receiver: address): """ @notice Transfer a single token from the pool to receiver. @dev This function is called by `remove_liquidity` and @@ -387,22 +363,12 @@ def _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) - - assert ERC20(_coin).transfer( - receiver, _amount, default_return_value=True - ) + assert ERC20(_coin).transfer(receiver, _amount, default_return_value=True) # -------------------------- AMM Main Functions ------------------------------ -@payable @external @nonreentrant("lock") def exchange( @@ -410,7 +376,6 @@ def exchange( j: uint256, dx: uint256, min_dy: uint256, - use_eth: bool = False, receiver: address = msg.sender ) -> uint256: """ @@ -425,45 +390,10 @@ def exchange( """ return self._exchange( msg.sender, - msg.value, - i, - j, - dx, - min_dy, - use_eth, - receiver, - empty(address), - empty(bytes32) - ) - - -@payable -@external -@nonreentrant('lock') -def exchange_underlying( - i: uint256, - j: uint256, - dx: uint256, - min_dy: uint256, - receiver: address = msg.sender -) -> uint256: - """ - @notice Exchange using native token transfers. - @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 - """ - return self._exchange( - msg.sender, - msg.value, i, j, dx, min_dy, - True, receiver, empty(address), empty(bytes32) @@ -477,7 +407,6 @@ def exchange_extended( j: uint256, dx: uint256, min_dy: uint256, - use_eth: bool, sender: address, receiver: address, cb: bytes32 @@ -502,24 +431,21 @@ def exchange_extended( assert cb != empty(bytes32) # dev: No callback specified return self._exchange( - sender, 0, i, j, dx, min_dy, use_eth, receiver, msg.sender, cb + sender, i, j, dx, min_dy, 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 """ @@ -563,33 +489,15 @@ def add_liquidity( if amounts[i] > 0: - 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: - - 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. - ) + self._transfer_in( + coins[i], + amounts[i], + 0, + empty(address), + empty(bytes32), + msg.sender, + empty(address), + ) amountsp[i] = xp[i] - xp_old[i] @@ -651,7 +559,6 @@ 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]: @@ -713,7 +620,7 @@ def remove_liquidity( # ---------------------------------- Transfers --------------------------- for i in range(N_COINS): - self._transfer_out(coins[i], d_balances[i], use_eth, receiver) + self._transfer_out(coins[i], d_balances[i], receiver) log RemoveLiquidity(msg.sender, balances, total_supply - _amount) @@ -767,7 +674,7 @@ def remove_liquidity_one_coin( self.balances[i] -= dy self.burnFrom(msg.sender, token_amount) - self._transfer_out(coins[i], dy, use_eth, receiver) + self._transfer_out(coins[i], dy, receiver) packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0) # Safe to use D from _calc_withdraw_one_coin here ---^ @@ -779,15 +686,6 @@ def remove_liquidity_one_coin( return dy -@external -@nonreentrant("lock") -def claim_admin_fees(): - """ - @notice Claim admin fees. Callable by anyone. - """ - self._claim_admin_fees() - - # -------------------------- Packing functions ------------------------------- @@ -858,12 +756,10 @@ 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 @@ -941,13 +837,14 @@ def _exchange( ########################## TRANSFER IN <------- self._transfer_in( - coins[i], dx, dy, mvalue, + coins[i], + dx, dy, callbacker, callback_sig, # <-------- Callback method is called here. - sender, receiver, use_eth, + sender, receiver ) ########################## -------> TRANSFER OUT - self._transfer_out(coins[j], dy, use_eth, receiver) + self._transfer_out(coins[j], dy, receiver) # ------ Tweak price_scale with good initial guess for newton_D ---------- @@ -1189,10 +1086,7 @@ def _claim_admin_fees(): # 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) + self.balances[i] = ERC20(coins[i]).balanceOf(self) # If the pool has made no profits, `xcp_profit == xcp_profit_a` # and the pool gulps nothing in the previous step. @@ -1214,18 +1108,16 @@ 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: - - frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18 - claimed: uint256 = self.mint_relative(receiver, frac) + admin_share: uint256 = 0 + frac: uint256 = 0 + fee_receiver: address = Factory(self.factory).fee_receiver() + if fee_receiver != empty(address) and fees > 0: + frac = vprice * 10**18 / (vprice - fees) - 10**18 + admin_share = total_supply * frac / 10**18 xcp_profit -= fees * 2 - self.xcp_profit = xcp_profit - log ClaimAdminFee(receiver, claimed) - # ------------------------------------------- Recalculate D b/c we gulped. D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], self.xp(), 0) self.D = D @@ -1235,9 +1127,14 @@ def _claim_admin_fees(): # than old virtual price, since the claim process can result # in a small decrease in pool's value. - self.virtual_price = 10**18 * self.get_xcp(D) / self.totalSupply + self.virtual_price = 10**18 * self.get_xcp(D) / (total_supply + admin_share) self.xcp_profit_a = xcp_profit # <------------ Cache last claimed profit. + # Mint Admin Fee share: + if admin_share > 0: + assert self.mint(fee_receiver, admin_share) + log ClaimAdminFee(fee_receiver, admin_share) + @internal @view @@ -1601,24 +1498,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: """ From 3d0a62cb721f5cc71746d633d348370d08b7d6e5 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:10:27 +0200 Subject: [PATCH 02/53] remove claiming fees from remove_liquidity --- contracts/main/CurveTricryptoOptimizedWETH.vy | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index d4ae4f06..3171e245 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -560,7 +560,6 @@ def remove_liquidity( _amount: uint256, min_amounts: uint256[N_COINS], receiver: address = msg.sender, - claim_admin_fees: bool = True, ) -> uint256[N_COINS]: """ @notice This withdrawal method is very safe, does no complex math since @@ -576,10 +575,6 @@ def remove_liquidity( 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. - # -------------------------------------------------------- Burn LP tokens. total_supply: uint256 = self.totalSupply # <------ Get totalSupply before From b8e63933c17556a756244ae35bfad06cdcd93d08 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:10:44 +0200 Subject: [PATCH 03/53] amend docstring --- contracts/main/CurveTricryptoOptimizedWETH.vy | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 3171e245..d44c6775 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -568,7 +568,6 @@ def remove_liquidity( @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 From 247920abf8c5685c04546f7e9e2cfadd92741c91 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:25:23 +0200 Subject: [PATCH 04/53] move fee collection to the top of liquidity methods --- contracts/main/CurveTricryptoOptimizedWETH.vy | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index d44c6775..87239456 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -323,7 +323,6 @@ def _transfer_in( @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 callback_sig == empty(bytes32): @@ -360,7 +359,6 @@ def _transfer_out(_coin: address, _amount: uint256, receiver: address): `remove_liquidity_one` and `_exchange` methods. @params _coin Address 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 """ assert ERC20(_coin).transfer(receiver, _amount, default_return_value=True) @@ -384,7 +382,6 @@ 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 """ @@ -422,7 +419,6 @@ def exchange_extended( @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 @@ -450,6 +446,8 @@ def add_liquidity( @return uint256 Amount of LP tokens received by the `receiver """ + self._claim_admin_fees() # <--------------------------- Claim admin fees. + A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) @@ -549,8 +547,6 @@ def add_liquidity( receiver, amounts, d_token_fee, token_supply, packed_price_scale ) - self._claim_admin_fees() # <--------------------------- Claim admin fees. - return d_token @@ -566,7 +562,6 @@ def remove_liquidity( 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 @return uint256[3] Amount of pool tokens received by the `receiver` """ @@ -627,7 +622,6 @@ def remove_liquidity_one_coin( token_amount: uint256, i: uint256, min_amount: uint256, - use_eth: bool = False, receiver: address = msg.sender ) -> uint256: """ @@ -637,11 +631,12 @@ 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` """ + self._claim_admin_fees() # <- Claim admin fees before removing liquidity. + A_gamma: uint256[2] = self._A_gamma() dy: uint256 = 0 @@ -650,9 +645,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( From 3373977e916738be7cc1752dbb6a76985685f638 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 2 Aug 2023 12:53:39 +0200 Subject: [PATCH 05/53] raise fees while pool is ramping and only allow claiming admin fees when pool is not ramping; add comments on how claiming admin fees involves gulping and should be used carefully when optimistic transfers are involved --- contracts/main/CurveTricryptoOptimizedWETH.vy | 112 +++++++++++++++--- 1 file changed, 98 insertions(+), 14 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 87239456..c8d9c13e 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -4,7 +4,7 @@ @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). +@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. """ @@ -160,6 +160,7 @@ future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. # (i.e. self.future_A_gamma_time < block.timestamp), the variable is left # and not set to 0. +stored_balances: HashMap[address, uint256] # <---- Cached pool token balances. balances: public(uint256[N_COINS]) D: public(uint256) xcp_profit: public(uint256) @@ -305,6 +306,7 @@ def _transfer_in( callback_sig: bytes32, sender: address, receiver: address, + expect_optimistic_transfer: bool, ): """ @notice Transfers `_coin` from `sender` to `self` and calls `callback_sig` @@ -325,7 +327,12 @@ def _transfer_in( @params receiver address to transfer `_coin` to. """ - if callback_sig == empty(bytes32): + if expect_optimistic_transfer: + + b: uint256 = ERC20(_coin).balanceOf(self) + assert b - self.stored_balances[_coin] == dx # dev: user didn't give us coins + + elif callback_sig == empty(bytes32): assert ERC20(_coin).transferFrom( sender, self, dx, default_return_value=True @@ -350,6 +357,8 @@ def _transfer_in( # ^------ note: dx cannot # be 0, so the contract MUST receive some _coin. + self.stored_balances[_coin] += dx + @internal def _transfer_out(_coin: address, _amount: uint256, receiver: address): @@ -362,6 +371,7 @@ def _transfer_out(_coin: address, _amount: uint256, receiver: address): @params receiver Address to send the tokens to """ assert ERC20(_coin).transfer(receiver, _amount, default_return_value=True) + self.stored_balances[_coin] -= _amount # -------------------------- AMM Main Functions ------------------------------ @@ -393,7 +403,8 @@ def exchange( min_dy, receiver, empty(address), - empty(bytes32) + empty(bytes32), + False, ) @@ -410,8 +421,6 @@ def exchange_extended( ) -> 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. @@ -427,8 +436,51 @@ def exchange_extended( assert cb != empty(bytes32) # dev: No callback specified return self._exchange( - sender, i, j, dx, min_dy, receiver, msg.sender, cb - ) # callbacker should never be self ------------------^ + sender, + i, + j, + dx, + min_dy, + receiver, + msg.sender, + cb, + False + ) + + +@external +@nonreentrant('lock') +def exchange_received( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + receiver: address, +) -> uint256: + """ + @notice Exchange: but user must transfer dx amount of coin[i] tokens to pool first + @dev Use-case is to reduce the number of redundant ERC20 token + transfers in zaps. Primarily for dex aggregators. + @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 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` + """ + return self._exchange( + msg.sender, + i, + j, + dx, + min_dy, + receiver, + empty(address), + empty(bytes32), + True, + ) @external @@ -446,6 +498,11 @@ def add_liquidity( @return uint256 Amount of LP tokens received by the `receiver """ + # Claiming admin fees involves gulping tokens: syncing token balances to + # stored balances. This can interfere with optimistic transfers. These + # optimistic transfers are enabled only for _exchange related methods, + # where admin fee is not claimed, and disabled for adding and removing + # liquidity. It is, hence, fine to claim admin fees here: self._claim_admin_fees() # <--------------------------- Claim admin fees. A_gamma: uint256[2] = self._A_gamma() @@ -495,6 +552,7 @@ def add_liquidity( empty(bytes32), msg.sender, empty(address), + False, ) amountsp[i] = xp[i] - xp_old[i] @@ -635,6 +693,11 @@ def remove_liquidity_one_coin( @return Amount of tokens at index i received by the `receiver` """ + # Claiming admin fees involves gulping tokens: syncing token balances to + # stored balances. This can interfere with optimistic transfers. These + # optimistic transfers are enabled only for _exchange related methods, + # where admin fee is not claimed, and disabled for adding and removing + # liquidity. It is, hence, fine to claim admin fees here: self._claim_admin_fees() # <- Claim admin fees before removing liquidity. A_gamma: uint256[2] = self._A_gamma() @@ -748,7 +811,8 @@ def _exchange( min_dy: uint256, receiver: address, callbacker: address, - callback_sig: bytes32 + callback_sig: bytes32, + expect_optimistic_transfer: bool, ) -> uint256: assert i != j # dev: coin index out of range @@ -826,8 +890,9 @@ def _exchange( coins[i], dx, dy, callbacker, callback_sig, # <-------- Callback method is called here. - sender, receiver - ) + sender, receiver, + expect_optimistic_transfer # <---- If True, pool expects dx tokens to + ) # be transferred in. ########################## -------> TRANSFER OUT self._transfer_out(coins[j], dy, receiver) @@ -1063,7 +1128,12 @@ def _claim_admin_fees(): # 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: + # 3. Pool parameters are being ramped. + if ( + xcp_profit <= xcp_profit_a or + total_supply < 10**18 or + self.future_A_gamma_time < block.timestamp + ): return # Claim tokens belonging to the admin here. This is done by 'gulping' @@ -1072,6 +1142,8 @@ def _claim_admin_fees(): # outgoing tokens excluding fees. Following 'gulps' fees: for i in range(N_COINS): + # Note: do not add gulping of tokens in external methods that involve + # optimistic token transfers. self.balances[i] = ERC20(coins[i]).balanceOf(self) # If the pool has made no profits, `xcp_profit == xcp_profit_a` @@ -1106,6 +1178,7 @@ def _claim_admin_fees(): # ------------------------------------------- Recalculate D b/c we gulped. D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], self.xp(), 0) + # TODO: Add a safety check here for D self.D = D # ------------------- Recalculate virtual_price following admin fee claim. @@ -1113,12 +1186,17 @@ def _claim_admin_fees(): # than old virtual price, since the claim process can result # in a small decrease in pool's value. - self.virtual_price = 10**18 * self.get_xcp(D) / (total_supply + admin_share) - self.xcp_profit_a = xcp_profit # <------------ Cache last claimed profit. + vprice = 10**18 * self.get_xcp(D) / (total_supply + admin_share) + # TODO: Add a safety check here to ensure vprice cannot be manipulated too + # high or too low. + self.virtual_price = vprice + + if xcp_profit > xcp_profit_a: + self.xcp_profit_a = xcp_profit # <-------- Cache last claimed profit. # Mint Admin Fee share: if admin_share > 0: - assert self.mint(fee_receiver, admin_share) + self.mint(fee_receiver, admin_share) log ClaimAdminFee(fee_receiver, admin_share) @@ -1168,7 +1246,13 @@ def _A_gamma() -> uint256[2]: @internal @view def _fee(xp: uint256[N_COINS]) -> uint256: + fee_params: uint256[3] = self._unpack(self.packed_fee_params) + + if self.future_A_gamma_time < block.timestamp: + fee_params[0] = MAX_FEE # mid_fee is MAX_FEE during ramping + fee_params[1] = MAX_FEE # out_fee is MAX_FEE during ramping + f: uint256 = MATH.reduction_coefficient(xp, fee_params[2]) return unsafe_div( fee_params[0] * f + fee_params[1] * (10**18 - f), From b3b86d51a99616b88865da7bad8e9bc3cbe0c0f2 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:31:13 +0200 Subject: [PATCH 06/53] add MIN_GULP_INTERVAL logic --- contracts/main/CurveTricryptoOptimizedWETH.vy | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index c8d9c13e..e573485d 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -189,9 +189,12 @@ NOISE_FEE: constant(uint256) = 10**5 # <---------------------------- 0.1 BPS. # ----------------------- Admin params --------------------------------------- admin_actions_deadline: public(uint256) +last_gulp_timestamp: uint256 # <------ Records the block timestamp when admin +# fee was claimed. ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 MIN_RAMP_TIME: constant(uint256) = 86400 +MIN_GULP_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 @@ -1123,16 +1126,19 @@ def _claim_admin_fees(): 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 + last_gulp_timestamp: uint256 = self.last_gulp_timestamp # 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. # 3. Pool parameters are being ramped. + # 4. If time passed since last gulp is less than MIN_GULP_INTERVAL. if ( xcp_profit <= xcp_profit_a or total_supply < 10**18 or - self.future_A_gamma_time < block.timestamp + self.future_A_gamma_time < block.timestamp or + block.timestamp - last_gulp_timestamp < MIN_GULP_INTERVAL ): return @@ -1145,6 +1151,9 @@ def _claim_admin_fees(): # Note: do not add gulping of tokens in external methods that involve # optimistic token transfers. self.balances[i] = ERC20(coins[i]).balanceOf(self) + # TODO: Add safety checks to ensure balances are not wildly manipulated. + + self.last_gulp_timestamp = block.timestamp # If the pool has made no profits, `xcp_profit == xcp_profit_a` # and the pool gulps nothing in the previous step. @@ -1194,7 +1203,7 @@ def _claim_admin_fees(): if xcp_profit > xcp_profit_a: self.xcp_profit_a = xcp_profit # <-------- Cache last claimed profit. - # Mint Admin Fee share: + # -------------------------------------------------- Mint Admin Fee share. if admin_share > 0: self.mint(fee_receiver, admin_share) log ClaimAdminFee(fee_receiver, admin_share) From 5652a3d3ac701b0786ea01e579b4d86035cff18a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 2 Aug 2023 15:40:41 +0200 Subject: [PATCH 07/53] convert some methods to pure to facilitate easier handling of state --- contracts/main/CurveTricryptoOptimizedWETH.vy | 134 +++++++++++------- 1 file changed, 86 insertions(+), 48 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index e573485d..b4beabe0 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -194,7 +194,7 @@ last_gulp_timestamp: uint256 # <------ Records the block timestamp when admin ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 MIN_RAMP_TIME: constant(uint256) = 86400 -MIN_GULP_INTERVAL: constant(uint256) = 86400 +MIN_GULP_INTERVAL: constant(uint256) = 3600 MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 100 MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS @@ -329,17 +329,22 @@ def _transfer_in( @params sender address to transfer `_coin` from. @params receiver address to transfer `_coin` to. """ + received_amounts: uint256 = 0 + coin_balance: uint256 = ERC20(_coin).balanceOf(self) if expect_optimistic_transfer: - b: uint256 = ERC20(_coin).balanceOf(self) - assert b - self.stored_balances[_coin] == dx # dev: user didn't give us coins + received_amounts = coin_balance - self.stored_balances[_coin] elif callback_sig == empty(bytes32): assert ERC20(_coin).transferFrom( - sender, self, dx, default_return_value=True + sender, + self, + dx, + default_return_value=True ) + received_amounts = ERC20(_coin).balanceOf(self) - coin_balance else: @@ -348,7 +353,6 @@ def _transfer_in( # 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( @@ -356,10 +360,9 @@ def _transfer_in( _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. + received_amounts = ERC20(_coin).balanceOf(self) - coin_balance + assert received_amounts == dx # dev: user didn't give us coins self.stored_balances[_coin] += dx @@ -555,7 +558,7 @@ def add_liquidity( empty(bytes32), msg.sender, empty(address), - False, + False, # <--------------------- Disable optimistic transfers. ) amountsp[i] = xp[i] - xp_old[i] @@ -577,7 +580,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 @@ -1121,45 +1124,55 @@ def _claim_admin_fees(): """ @notice Claims admin fees and sends it to fee_receiver set in the factory. """ + + # --------------------- Check if fees can be claimed --------------------- + + # Disable fee claiming if: + # 1. If time passed since last gulp is less than MIN_GULP_INTERVAL. + # 2. Pool parameters are being ramped. + + if ( + block.timestamp - self.last_gulp_timestamp < MIN_GULP_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 - last_gulp_timestamp: uint256 = self.last_gulp_timestamp # 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. - # 3. Pool parameters are being ramped. - # 4. If time passed since last gulp is less than MIN_GULP_INTERVAL. - if ( - xcp_profit <= xcp_profit_a or - total_supply < 10**18 or - self.future_A_gamma_time < block.timestamp or - block.timestamp - last_gulp_timestamp < MIN_GULP_INTERVAL - ): + + if (xcp_profit <= xcp_profit_a or total_supply < 10**18): return + # ---------- Conditions met to claim admin fees: compute state. ---------- + + vprice: uint256 = self.virtual_price + packed_price_scale: uint256 = self.price_scale_packed + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + fee_receiver: address = Factory(self.factory).fee_receiver() + # 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: + gulped_balances: uint256[N_COINS] = empty(uint256[N_COINS]) for i in range(N_COINS): # Note: do not add gulping of tokens in external methods that involve # optimistic token transfers. - self.balances[i] = ERC20(coins[i]).balanceOf(self) + gulped_balances[i] = ERC20(coins[i]).balanceOf(self) # TODO: Add safety checks to ensure balances are not wildly manipulated. - self.last_gulp_timestamp = block.timestamp - # If the pool has made no profits, `xcp_profit == xcp_profit_a` # and the pool gulps nothing in the previous step. - vprice: uint256 = self.virtual_price - # Admin fees are calculated as follows. # 1. Calculate accrued profit since last claim. `xcp_profit` # is the current profits. `xcp_profit_a` is the profits @@ -1177,47 +1190,62 @@ def _claim_admin_fees(): # of the pool in LP tokens. admin_share: uint256 = 0 frac: uint256 = 0 - fee_receiver: address = Factory(self.factory).fee_receiver() if fee_receiver != empty(address) and fees > 0: + # -------------------------------- Calculate admin share to be minted. frac = vprice * 10**18 / (vprice - fees) - 10**18 admin_share = total_supply * frac / 10**18 + + # ------ Subtract fees from profits that will be used for rebalancing. xcp_profit -= fees * 2 - self.xcp_profit = xcp_profit # ------------------------------------------- Recalculate D b/c we gulped. - D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], self.xp(), 0) - # TODO: Add a safety check here for D - self.D = D + D: uint256 = MATH.newton_D( + A_gamma[0], + A_gamma[1], + self.xp(gulped_balances, packed_price_scale, precisions), + 0 + ) # ------------------- 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. - vprice = 10**18 * self.get_xcp(D) / (total_supply + admin_share) - # TODO: Add a safety check here to ensure vprice cannot be manipulated too - # high or too low. - self.virtual_price = vprice + vprice = ( + 10**18 * self.get_xcp(D, packed_price_scale) / + (total_supply + admin_share) + ) + if vprice < 10**18: + return # <------ Virtual price goes below 10**18 > Do not claim fees. - if xcp_profit > xcp_profit_a: - self.xcp_profit_a = xcp_profit # <-------- Cache last claimed profit. + # ------------ Admin fee claiming is safe. Can mutate state. ------------- - # -------------------------------------------------- Mint Admin Fee share. if admin_share > 0: - self.mint(fee_receiver, admin_share) + self.mint(fee_receiver, admin_share) # <------- Mint Admin Fee share. log ClaimAdminFee(fee_receiver, admin_share) + self.balances = gulped_balances # <---------- Commit gulping of balances. + self.xcp_profit = xcp_profit + self.last_gulp_timestamp = block.timestamp # <--------- Update gulp time. + self.virtual_price = vprice + self.D = D -@internal -@view -def xp() -> uint256[N_COINS]: + if xcp_profit > xcp_profit_a: + self.xcp_profit_a = xcp_profit # <-------- Cache last claimed profit. - result: uint256[N_COINS] = self.balances - packed_prices: uint256 = self.price_scale_packed - precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) +@internal +@pure +def xp( + balances: uint256[N_COINS], + price_scale_packed: uint256, + precisions: uint256[N_COINS] +) -> uint256[N_COINS]: + + 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] result[i] = result[i] * p / PRECISION @@ -1270,12 +1298,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): @@ -1681,7 +1709,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 @@ -1763,7 +1794,14 @@ def fee() -> uint256: removed. @return uint256 fee bps. """ - return self._fee(self.xp()) + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + return self._fee( + self.xp( + self.balances, + self.price_scale_packed, + precisions + ) + ) @view From 0c19f16d946717e8bbf54559790e686f577531c8 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 2 Aug 2023 18:45:09 +0200 Subject: [PATCH 08/53] bye bye exchange_extended. you were good, but nobody used you --- contracts/main/CurveTricryptoOptimizedWETH.vy | 77 ++----------------- 1 file changed, 6 insertions(+), 71 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index b4beabe0..66b464dd 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -194,7 +194,7 @@ last_gulp_timestamp: uint256 # <------ Records the block timestamp when admin ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 MIN_RAMP_TIME: constant(uint256) = 86400 -MIN_GULP_INTERVAL: constant(uint256) = 3600 +MIN_GULP_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 @@ -304,11 +304,7 @@ def __init__( def _transfer_in( _coin: address, dx: uint256, - dy: uint256, - callbacker: address, - callback_sig: bytes32, sender: address, - receiver: address, expect_optimistic_transfer: bool, ): """ @@ -336,7 +332,7 @@ def _transfer_in( received_amounts = coin_balance - self.stored_balances[_coin] - elif callback_sig == empty(bytes32): + else: assert ERC20(_coin).transferFrom( sender, @@ -346,22 +342,6 @@ def _transfer_in( ) received_amounts = ERC20(_coin).balanceOf(self) - coin_balance - 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. - raw_call( - callbacker, - concat( - slice(callback_sig, 0, 4), - _abi_encode(sender, receiver, _coin, dx, dy) - ) - ) - received_amounts = ERC20(_coin).balanceOf(self) - coin_balance - assert received_amounts == dx # dev: user didn't give us coins self.stored_balances[_coin] += dx @@ -414,46 +394,6 @@ def exchange( ) -@external -@nonreentrant('lock') -def exchange_extended( - i: uint256, - j: uint256, - dx: uint256, - min_dy: uint256, - sender: address, - receiver: address, - cb: bytes32 -) -> uint256: - """ - @notice Exchange with callback method. - @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 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, - i, - j, - dx, - min_dy, - receiver, - msg.sender, - cb, - False - ) - - @external @nonreentrant('lock') def exchange_received( @@ -553,11 +493,7 @@ def add_liquidity( self._transfer_in( coins[i], amounts[i], - 0, - empty(address), - empty(bytes32), msg.sender, - empty(address), False, # <--------------------- Disable optimistic transfers. ) @@ -894,9 +830,8 @@ def _exchange( ########################## TRANSFER IN <------- self._transfer_in( coins[i], - dx, dy, - callbacker, callback_sig, # <-------- Callback method is called here. - sender, receiver, + dx, + sender, expect_optimistic_transfer # <---- If True, pool expects dx tokens to ) # be transferred in. @@ -1165,10 +1100,10 @@ def _claim_admin_fees(): gulped_balances: uint256[N_COINS] = empty(uint256[N_COINS]) for i in range(N_COINS): + # Note: do not add gulping of tokens in external methods that involve # optimistic token transfers. - gulped_balances[i] = ERC20(coins[i]).balanceOf(self) - # TODO: Add safety checks to ensure balances are not wildly manipulated. + gulped_balances[i] = self.stored_balances[coins[i]] # <-------------------- consider this. # If the pool has made no profits, `xcp_profit == xcp_profit_a` # and the pool gulps nothing in the previous step. From ed15aa113804b052bbedfa0c004fcf56aa0c3a0a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 2 Aug 2023 19:21:26 +0200 Subject: [PATCH 09/53] simplify claim admin fees a bit --- contracts/main/CurveTricryptoOptimizedWETH.vy | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 66b464dd..07d1d1fa 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1130,6 +1130,7 @@ def _claim_admin_fees(): # -------------------------------- Calculate admin share to be minted. frac = vprice * 10**18 / (vprice - fees) - 10**18 admin_share = total_supply * frac / 10**18 + total_supply += admin_share # ------ Subtract fees from profits that will be used for rebalancing. xcp_profit -= fees * 2 @@ -1147,10 +1148,7 @@ def _claim_admin_fees(): # than old virtual price, since the claim process can result # in a small decrease in pool's value. - vprice = ( - 10**18 * self.get_xcp(D, packed_price_scale) / - (total_supply + admin_share) - ) + vprice = 10**18 * self.get_xcp(D, packed_price_scale) / total_supply if vprice < 10**18: return # <------ Virtual price goes below 10**18 > Do not claim fees. From 19cd912c103b6a850d4e09be6771017db8fa7d96 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 2 Aug 2023 19:34:39 +0200 Subject: [PATCH 10/53] remove unneeded args from _exchange method --- contracts/main/CurveTricryptoOptimizedWETH.vy | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 07d1d1fa..781bc6ca 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -388,8 +388,6 @@ def exchange( dx, min_dy, receiver, - empty(address), - empty(bytes32), False, ) @@ -423,8 +421,6 @@ def exchange_received( dx, min_dy, receiver, - empty(address), - empty(bytes32), True, ) @@ -752,8 +748,6 @@ def _exchange( dx: uint256, min_dy: uint256, receiver: address, - callbacker: address, - callback_sig: bytes32, expect_optimistic_transfer: bool, ) -> uint256: From 184eeb8401b8dc720f7dafa99a8abdbd26791dab Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:38:07 +0200 Subject: [PATCH 11/53] adjust tests --- contracts/main/CurveTricryptoOptimizedWETH.vy | 4 +- scripts/deployment_utils.py | 14 -- tests/boa/profiling/test_boa_profile.py | 46 +--- .../pool/stateful/test_admin_fee_claim.py | 8 +- .../pool/stateful/test_gas_realistic.py | 4 - tests/boa/unitary/pool/stateful/test_ramp.py | 7 - .../unitary/pool/stateful/test_stateful.py | 4 - tests/boa/unitary/pool/test_a_gamma.py | 4 + tests/boa/unitary/pool/test_callback.py | 150 ------------ .../boa/unitary/pool/test_deposit_withdraw.py | 88 ------- tests/boa/unitary/pool/test_exchange.py | 108 --------- .../unitary/pool/test_exchange_received.py | 1 + tests/boa/unitary/pool/test_use_eth.py | 215 ------------------ 13 files changed, 16 insertions(+), 637 deletions(-) delete mode 100644 tests/boa/unitary/pool/test_callback.py create mode 100644 tests/boa/unitary/pool/test_exchange_received.py delete mode 100644 tests/boa/unitary/pool/test_use_eth.py diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 781bc6ca..fe28ad57 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1097,7 +1097,7 @@ def _claim_admin_fees(): # Note: do not add gulping of tokens in external methods that involve # optimistic token transfers. - gulped_balances[i] = self.stored_balances[coins[i]] # <-------------------- consider this. + gulped_balances[i] = self.stored_balances[coins[i]] # If the pool has made no profits, `xcp_profit == xcp_profit_a` # and the pool gulps nothing in the previous step. @@ -1146,7 +1146,7 @@ def _claim_admin_fees(): if vprice < 10**18: return # <------ Virtual price goes below 10**18 > Do not claim fees. - # ------------ Admin fee claiming is safe. Can mutate state. ------------- + # ---------------------------- Update State ------------------------------ if admin_share > 0: self.mint(fee_receiver, admin_share) # <------- Mint Admin Fee share. diff --git a/scripts/deployment_utils.py b/scripts/deployment_utils.py index efbf3786..7e2eccec 100644 --- a/scripts/deployment_utils.py +++ b/scripts/deployment_utils.py @@ -344,20 +344,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/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/pool/stateful/test_admin_fee_claim.py b/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py index 96be6c32..8222e7f3 100644 --- a/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py +++ b/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py @@ -9,6 +9,8 @@ STEP_COUNT = 100 NO_CHANGE = 2**256 - 1 +# TODO: Test admin fee claims considering the various cases where it is disallowed. # noqa: E501 + def approx(x1, x2, precision): return abs(log(x1 / x2)) <= precision @@ -52,12 +54,6 @@ 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): - - with self.upkeep_on_claim(): - self.swap.claim_admin_fees() - def test_admin_fee(swap, views_contract, users, pool_coins, tricrypto_factory): from hypothesis import settings diff --git a/tests/boa/unitary/pool/stateful/test_gas_realistic.py b/tests/boa/unitary/pool/stateful/test_gas_realistic.py index 61534e6e..2e494abf 100644 --- a/tests/boa/unitary/pool/stateful/test_gas_realistic.py +++ b/tests/boa/unitary/pool/stateful/test_gas_realistic.py @@ -85,10 +85,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..69af1f0c 100644 --- a/tests/boa/unitary/pool/stateful/test_ramp.py +++ b/tests/boa/unitary/pool/stateful/test_ramp.py @@ -58,9 +58,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 +86,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 ) diff --git a/tests/boa/unitary/pool/stateful/test_stateful.py b/tests/boa/unitary/pool/stateful/test_stateful.py index 81a1c19f..f015a72d 100644 --- a/tests/boa/unitary/pool/stateful/test_stateful.py +++ b/tests/boa/unitary/pool/stateful/test_stateful.py @@ -127,10 +127,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_a_gamma.py b/tests/boa/unitary/pool/test_a_gamma.py index 715b7678..7e09cf0a 100644 --- a/tests/boa/unitary/pool/test_a_gamma.py +++ b/tests/boa/unitary/pool/test_a_gamma.py @@ -48,3 +48,7 @@ def test_ramp_A_gamma(swap, factory_admin): ) < 1e-4 * A_gamma_initial[1] ) + + +# TODO: Add check for fees during ramps +# TODO: Add test to ensure admin fees are not being claimed 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..e06be852 100644 --- a/tests/boa/unitary/pool/test_deposit_withdraw.py +++ b/tests/boa/unitary/pool/test_deposit_withdraw.py @@ -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( @@ -403,41 +353,3 @@ def test_immediate_withdraw_one( # 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..3252d239 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 @@ -58,110 +57,3 @@ def test_exchange_all( assert d_balance_i == amount 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 - ) -) -@settings(**SETTINGS) -def test_exchange_from_eth( - swap_with_deposit, - views_contract, - coins, - user, - amount, - j, -): - - amount = amount * 10**18 // INITIAL_PRICES[2] - - calculated = views_contract.get_dy(2, j, amount, swap_with_deposit) - - measured_i = boa.env.get_balance(user) - measured_j = coins[j].balanceOf(user) - d_balance_i = swap_with_deposit.balances(2) - 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 - ) - - measured_i -= boa.env.get_balance(user) - measured_j = coins[j].balanceOf(user) - measured_j - d_balance_i = swap_with_deposit.balances(2) - d_balance_i - d_balance_j = swap_with_deposit.balances(j) - d_balance_j - - assert amount == measured_i - assert calculated == measured_j - - assert d_balance_i == amount - assert -d_balance_j == measured_j - - -@pytest.mark.parametrize("i", [0, 1]) -@given( - amount=strategy( - "uint256", min_value=10**10, max_value=2 * 10**6 * 10**18 - ) -) -@settings(**SETTINGS) -def test_exchange_into_eth( - swap_with_deposit, - views_contract, - coins, - user, - amount, - i, -): - - 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 - - assert amount == measured_i - assert calculated == measured_j - - assert d_balance_i == amount - assert -d_balance_j == measured_j - - -@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) - ) - - -@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_exchange_received.py b/tests/boa/unitary/pool/test_exchange_received.py new file mode 100644 index 00000000..3e2edc91 --- /dev/null +++ b/tests/boa/unitary/pool/test_exchange_received.py @@ -0,0 +1 @@ +# TODO: Add test exchange_received 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] From 956311b6f8429773f14c54ec06551c7bd95e5bf7 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 3 Aug 2023 18:33:02 +0200 Subject: [PATCH 12/53] goodbye gulp. you were a feature too many --- contracts/main/CurveTricryptoOptimizedWETH.vy | 15 --------------- tests/boa/unitary/pool/stateful/stateful_base.py | 11 +++++++---- tests/boa/unitary/views/test_get_dx.py | 3 +++ 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index fe28ad57..42629f84 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1087,21 +1087,6 @@ def _claim_admin_fees(): precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) fee_receiver: address = Factory(self.factory).fee_receiver() - # 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: - - gulped_balances: uint256[N_COINS] = empty(uint256[N_COINS]) - for i in range(N_COINS): - - # Note: do not add gulping of tokens in external methods that involve - # optimistic token transfers. - gulped_balances[i] = self.stored_balances[coins[i]] - - # If the pool has made no profits, `xcp_profit == xcp_profit_a` - # and the pool gulps nothing in the previous step. - # Admin fees are calculated as follows. # 1. Calculate accrued profit since last claim. `xcp_profit` # is the current profits. `xcp_profit_a` is the profits diff --git a/tests/boa/unitary/pool/stateful/stateful_base.py b/tests/boa/unitary/pool/stateful/stateful_base.py index 50a4a37a..2e00d525 100644 --- a/tests/boa/unitary/pool/stateful/stateful_base.py +++ b/tests/boa/unitary/pool/stateful/stateful_base.py @@ -220,13 +220,16 @@ 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 + stored_balances = [c.internal.stored_balances(c) for c in self.coins] for i in range(3): - assert self.balances[i] == balances[i] == balances_of[i] + assert ( + self.balances[i] + == balances[i] + == balances_of[i] + == stored_balances[i] + ) @invariant() def lp_token_total_supply(self): 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) From 2a90f9f439aa1f163aa63809c00e3c5a63d87e71 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 3 Aug 2023 18:37:01 +0200 Subject: [PATCH 13/53] remove balances update --- contracts/main/CurveTricryptoOptimizedWETH.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 42629f84..8c63b345 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1086,6 +1086,7 @@ def _claim_admin_fees(): packed_price_scale: uint256 = self.price_scale_packed precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) fee_receiver: address = Factory(self.factory).fee_receiver() + balances: uint256[N_COINS] = self.balances # Admin fees are calculated as follows. # 1. Calculate accrued profit since last claim. `xcp_profit` @@ -1118,7 +1119,7 @@ def _claim_admin_fees(): D: uint256 = MATH.newton_D( A_gamma[0], A_gamma[1], - self.xp(gulped_balances, packed_price_scale, precisions), + self.xp(balances, packed_price_scale, precisions), 0 ) @@ -1137,7 +1138,6 @@ def _claim_admin_fees(): self.mint(fee_receiver, admin_share) # <------- Mint Admin Fee share. log ClaimAdminFee(fee_receiver, admin_share) - self.balances = gulped_balances # <---------- Commit gulping of balances. self.xcp_profit = xcp_profit self.last_gulp_timestamp = block.timestamp # <--------- Update gulp time. self.virtual_price = vprice From 1a6e8989f858214678d08634276f31134b33d81c Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:52:24 +0200 Subject: [PATCH 14/53] wip: admin fee in individual tokens proposal --- contracts/main/CurveTricryptoOptimizedWETH.vy | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 8c63b345..4dcfd64a 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -127,7 +127,7 @@ event StopRampA: event ClaimAdminFee: admin: indexed(address) - tokens: uint256 + tokens: uint256[N_COINS] # ----------------------- Storage/State Variables ---------------------------- @@ -1070,14 +1070,15 @@ def _claim_admin_fees(): 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): + if (xcp_profit <= xcp_profit_a or current_lp_token_supply < 10**18): return # ---------- Conditions met to claim admin fees: compute state. ---------- @@ -1109,38 +1110,50 @@ def _claim_admin_fees(): # -------------------------------- Calculate admin share to be minted. frac = vprice * 10**18 / (vprice - fees) - 10**18 - admin_share = total_supply * frac / 10**18 - total_supply += admin_share + admin_share = current_lp_token_supply * frac / 10**18 # ------ Subtract fees from profits that will be used for rebalancing. xcp_profit -= fees * 2 - # ------------------------------------------- Recalculate D b/c we gulped. - D: uint256 = MATH.newton_D( - A_gamma[0], - A_gamma[1], - self.xp(balances, packed_price_scale, precisions), - 0 - ) - # ------------------- 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. + 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 + ) - vprice = 10**18 * self.get_xcp(D, packed_price_scale) / total_supply + # Do not claim fees if doing so causes virtual price to drop below 10**18. if vprice < 10**18: - return # <------ Virtual price goes below 10**18 > Do not claim fees. + return # ---------------------------- Update State ------------------------------ if admin_share > 0: - self.mint(fee_receiver, admin_share) # <------- Mint Admin Fee share. - log ClaimAdminFee(fee_receiver, admin_share) + + # self.mint(fee_receiver, admin_share) # <------- Mint Admin Fee share. + + # TODO: Get the following reviewed: + admin_tokens: uint256[N_COINS] = empty(uint256[N_COINS]) + for i in range(N_COINS): + admin_tokens[i] = ( + balances[i] * admin_share / + total_supply_including_admin_share + ) + + # Transfer tokens to admin: + assert ERC20(coins[i]).transfer(fee_receiver, admin_tokens[i]) + + log ClaimAdminFee(fee_receiver, admin_tokens) + + # self.virtual_price = vprice self.xcp_profit = xcp_profit self.last_gulp_timestamp = block.timestamp # <--------- Update gulp time. - self.virtual_price = vprice self.D = D if xcp_profit > xcp_profit_a: From b76e26709eca5029987cafbdf8d12dd92ed3d438 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:03:32 +0200 Subject: [PATCH 15/53] some ironing out of var names and remove updating D in storage since we dont gulp anymore --- contracts/main/CurveTricryptoOptimizedWETH.vy | 44 ++++++------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 4dcfd64a..bfd512cc 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -189,12 +189,11 @@ NOISE_FEE: constant(uint256) = 10**5 # <---------------------------- 0.1 BPS. # ----------------------- Admin params --------------------------------------- admin_actions_deadline: public(uint256) -last_gulp_timestamp: uint256 # <------ Records the block timestamp when admin -# fee was claimed. +last_admin_fee_claim_timestamp: uint256 ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 MIN_RAMP_TIME: constant(uint256) = 86400 -MIN_GULP_INTERVAL: 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 @@ -440,13 +439,6 @@ def add_liquidity( @return uint256 Amount of LP tokens received by the `receiver """ - # Claiming admin fees involves gulping tokens: syncing token balances to - # stored balances. This can interfere with optimistic transfers. These - # optimistic transfers are enabled only for _exchange related methods, - # where admin fee is not claimed, and disabled for adding and removing - # liquidity. It is, hence, fine to claim admin fees here: - self._claim_admin_fees() # <--------------------------- Claim admin fees. - A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) @@ -543,6 +535,8 @@ def add_liquidity( receiver, amounts, d_token_fee, token_supply, packed_price_scale ) + self._claim_admin_fees() # <--------------------------- Claim admin fees. + return d_token @@ -631,13 +625,6 @@ def remove_liquidity_one_coin( @return Amount of tokens at index i received by the `receiver` """ - # Claiming admin fees involves gulping tokens: syncing token balances to - # stored balances. This can interfere with optimistic transfers. These - # optimistic transfers are enabled only for _exchange related methods, - # where admin fee is not claimed, and disabled for adding and removing - # liquidity. It is, hence, fine to claim admin fees here: - self._claim_admin_fees() # <- Claim admin fees before removing liquidity. - A_gamma: uint256[2] = self._A_gamma() dy: uint256 = 0 @@ -670,6 +657,8 @@ def remove_liquidity_one_coin( msg.sender, token_amount, i, dy, approx_fee, packed_price_scale ) + self._claim_admin_fees() # <--------------------------- Claim admin fees. + return dy @@ -1057,12 +1046,15 @@ def _claim_admin_fees(): # --------------------- Check if fees can be claimed --------------------- # Disable fee claiming if: - # 1. If time passed since last gulp is less than MIN_GULP_INTERVAL. + # 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 - self.last_gulp_timestamp < MIN_GULP_INTERVAL or - self.future_A_gamma_time < block.timestamp + block.timestamp - last_claim_time < MIN_ADMIN_FEE_CLAIM_INTERVAL + or self.future_A_gamma_time < block.timestamp ): return @@ -1116,9 +1108,6 @@ def _claim_admin_fees(): xcp_profit -= fees * 2 # ------------------- 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. total_supply_including_admin_share: uint256 = ( current_lp_token_supply + admin_share ) @@ -1135,11 +1124,9 @@ def _claim_admin_fees(): if admin_share > 0: - # self.mint(fee_receiver, admin_share) # <------- Mint Admin Fee share. - - # TODO: Get the following reviewed: admin_tokens: uint256[N_COINS] = empty(uint256[N_COINS]) for i in range(N_COINS): + admin_tokens[i] = ( balances[i] * admin_share / total_supply_including_admin_share @@ -1150,11 +1137,8 @@ def _claim_admin_fees(): log ClaimAdminFee(fee_receiver, admin_tokens) - # self.virtual_price = vprice - self.xcp_profit = xcp_profit - self.last_gulp_timestamp = block.timestamp # <--------- Update gulp time. - self.D = D + self.last_admin_fee_claim_timestamp = block.timestamp if xcp_profit > xcp_profit_a: self.xcp_profit_a = xcp_profit # <-------- Cache last claimed profit. From d961100f57d8e0e315f29a7c8091f1aa77e96997 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:45:25 +0200 Subject: [PATCH 16/53] remove stored_balances and use balances instead --- contracts/main/CurveTricryptoOptimizedWETH.vy | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index bfd512cc..7b02674b 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -160,7 +160,6 @@ future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. # (i.e. self.future_A_gamma_time < block.timestamp), the variable is left # and not set to 0. -stored_balances: HashMap[address, uint256] # <---- Cached pool token balances. balances: public(uint256[N_COINS]) D: public(uint256) xcp_profit: public(uint256) @@ -301,7 +300,7 @@ def __init__( @internal def _transfer_in( - _coin: address, + _coin_idx: uint256, dx: uint256, sender: address, expect_optimistic_transfer: bool, @@ -325,28 +324,28 @@ def _transfer_in( @params receiver address to transfer `_coin` to. """ received_amounts: uint256 = 0 - coin_balance: uint256 = ERC20(_coin).balanceOf(self) + coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) if expect_optimistic_transfer: - received_amounts = coin_balance - self.stored_balances[_coin] + received_amounts = coin_balance - self.balances[_coin_idx] else: - assert ERC20(_coin).transferFrom( + assert ERC20(coins[_coin_idx]).transferFrom( sender, self, dx, default_return_value=True ) - received_amounts = ERC20(_coin).balanceOf(self) - coin_balance + received_amounts = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance - assert received_amounts == dx # dev: user didn't give us coins - self.stored_balances[_coin] += dx + assert received_amounts >= dx # dev: user didn't give us coins + self.balances[_coin_idx] += dx @internal -def _transfer_out(_coin: address, _amount: uint256, 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 @@ -355,8 +354,12 @@ def _transfer_out(_coin: address, _amount: uint256, receiver: address): @params _amount Amount of token to transfer out @params receiver Address to send the tokens to """ - assert ERC20(_coin).transfer(receiver, _amount, default_return_value=True) - self.stored_balances[_coin] -= _amount + assert ERC20(coins[_coin_idx]).transfer( + receiver, + _amount, + default_return_value=True + ) + self.balances[_coin_idx] -= _amount # -------------------------- AMM Main Functions ------------------------------ @@ -460,7 +463,7 @@ def add_liquidity( 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] @@ -478,8 +481,9 @@ def add_liquidity( if amounts[i] > 0: + # Updates self.balances after checking transferred in amounts: self._transfer_in( - coins[i], + i, amounts[i], msg.sender, False, # <--------------------- Disable optimistic transfers. @@ -557,7 +561,7 @@ def remove_liquidity( """ amount: uint256 = _amount balances: uint256[N_COINS] = self.balances - d_balances: uint256[N_COINS] = empty(uint256[N_COINS]) + withdraw_amounts: uint256[N_COINS] = empty(uint256[N_COINS]) # -------------------------------------------------------- Burn LP tokens. @@ -577,18 +581,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 @@ -599,11 +601,12 @@ def remove_liquidity( # ---------------------------------- Transfers --------------------------- for i in range(N_COINS): - self._transfer_out(coins[i], d_balances[i], receiver) + # _transfer_out updates self.balances: + 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 @@ -646,9 +649,11 @@ def remove_liquidity_one_coin( # ------------------------- Transfers ------------------------------------ - self.balances[i] -= dy + # Burn user's tokens: self.burnFrom(msg.sender, token_amount) - self._transfer_out(coins[i], dy, receiver) + + # _transfer_out updates self.balances: + self._transfer_out(i, dy, receiver) packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0) # Safe to use D from _calc_withdraw_one_coin here ---^ @@ -751,7 +756,6 @@ def _exchange( y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert. x0: uint256 = xp[i] # <--------------- if i > N_COINS, this will revert. xp[i] = x0 + dx - self.balances[i] = xp[i] packed_price_scale: uint256 = self.price_scale_packed price_scale: uint256[N_COINS - 1] = self._unpack_prices( @@ -801,7 +805,6 @@ 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: @@ -811,15 +814,19 @@ def _exchange( # ---------------------- Do Transfers in and out ------------------------- ########################## TRANSFER IN <------- + + # _transfer_in updates self.balances here: self._transfer_in( - coins[i], + i, dx, sender, expect_optimistic_transfer # <---- If True, pool expects dx tokens to ) # be transferred in. ########################## -------> TRANSFER OUT - self._transfer_out(coins[j], dy, receiver) + + # _transfer_out updates self.balances here: + self._transfer_out(j, dy, receiver) # ------ Tweak price_scale with good initial guess for newton_D ---------- @@ -1041,6 +1048,8 @@ def tweak_price( def _claim_admin_fees(): """ @notice Claims admin fees and sends it to fee_receiver set in the factory. + # TODO: test if this breaks the AMM! We're not minting LP tokens for + # the admin """ # --------------------- Check if fees can be claimed --------------------- @@ -1132,8 +1141,8 @@ def _claim_admin_fees(): total_supply_including_admin_share ) - # Transfer tokens to admin: - assert ERC20(coins[i]).transfer(fee_receiver, admin_tokens[i]) + # _transfer_out tokens to admin and update self.balances: + self._transfer_out(i, admin_tokens[i], fee_receiver) log ClaimAdminFee(fee_receiver, admin_tokens) From b98f89567073dc8a4f9bb3bc7b2744858e56299d Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 4 Aug 2023 18:28:51 +0200 Subject: [PATCH 17/53] update boa and start fixing tests --- contracts/main/CurveTricryptoOptimizedWETH.vy | 18 +++++++++--------- requirements.txt | 2 +- scripts/experiments/sim_dydx.py | 8 +++++++- tests/boa/unitary/math/test_get_p.py | 8 +++++++- tests/boa/unitary/math/test_get_p_expt.py | 9 ++++++++- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 7b02674b..9d248949 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -137,7 +137,7 @@ 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 +packed_precisions: immutable(uint256) MATH: public(immutable(Math)) coins: public(immutable(address[N_COINS])) @@ -238,7 +238,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, @@ -254,7 +254,7 @@ def __init__( symbol = _symbol coins = _coins - self.packed_precisions = packed_precisions # <------- Precisions of coins + packed_precisions = __packed_precisions # <------- Precisions of coins # are calculated as 10**(18 - coin.decimals()). self.initial_A_gamma = packed_A_gamma # <------------------- A and gamma. @@ -454,7 +454,7 @@ def add_liquidity( # --------------------- Get prices, balances ----------------------------- - precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + precisions: uint256[N_COINS] = self._unpack(packed_precisions) packed_price_scale: uint256 = self.price_scale_packed price_scale: uint256[N_COINS-1] = self._unpack_prices(packed_price_scale) @@ -750,7 +750,7 @@ def _exchange( A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances - precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + precisions: uint256[N_COINS] = self._unpack(packed_precisions) dy: uint256 = 0 y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert. @@ -1086,7 +1086,7 @@ def _claim_admin_fees(): vprice: uint256 = self.virtual_price packed_price_scale: uint256 = self.price_scale_packed - precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + precisions: uint256[N_COINS] = self._unpack(packed_precisions) fee_receiver: address = Factory(self.factory).fee_receiver() balances: uint256[N_COINS] = self.balances @@ -1270,7 +1270,7 @@ 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) + precisions: uint256[N_COINS] = self._unpack(packed_precisions) xp: uint256[N_COINS] = precisions D0: uint256 = 0 @@ -1712,7 +1712,7 @@ def fee() -> uint256: removed. @return uint256 fee bps. """ - precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + precisions: uint256[N_COINS] = self._unpack(packed_precisions) return self._fee( self.xp( self.balances, @@ -1843,7 +1843,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 self._unpack(packed_precisions) @external diff --git a/requirements.txt b/requirements.txt index f6826346..60d7e921 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,5 @@ pdbpp hypothesis>=6.68.1 # vyper and dev framework: -git+https://github.com/vyperlang/titanoboa +git+https://github.com/vyperlang/titanoboa.git@429fcd9fb6a1da2215dd8cf709965cce13ea93be vyper>=0.3.9 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/unitary/math/test_get_p.py b/tests/boa/unitary/math/test_get_p.py index 85eb231e..538b2fde 100644 --- a/tests/boa/unitary/math/test_get_p.py +++ b/tests/boa/unitary/math/test_get_p.py @@ -57,7 +57,13 @@ def _get_dydx_vyper(swap, i, j, 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(), swap.precisions() + ) for k in range(3): if k != i and k != j: diff --git a/tests/boa/unitary/math/test_get_p_expt.py b/tests/boa/unitary/math/test_get_p_expt.py index f1beb411..2cedf3c9 100644 --- a/tests/boa/unitary/math/test_get_p_expt.py +++ b/tests/boa/unitary/math/test_get_p_expt.py @@ -155,7 +155,14 @@ 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(), swap.precisions() + ) + D = swap.D() p = price_calc.get_p(xp, D, [A, gamma]) From 033bd427ff781aac7157f99636a0d8cd0c56e227 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 4 Aug 2023 18:35:56 +0200 Subject: [PATCH 18/53] fix test: use _immutables since packed precisions are immutables --- tests/boa/unitary/factory/test_deploy_pool.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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()) From 7eebe94d028d6a2c132ecddd832f23d7f3cfb06e Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 4 Aug 2023 21:10:36 +0200 Subject: [PATCH 19/53] fix tests: remove stored_balances --- tests/boa/unitary/pool/stateful/stateful_base.py | 8 +------- .../boa/unitary/pool/stateful/test_admin_fee_claim.py | 10 ++++++++++ tests/boa/unitary/pool/stateful/test_stateful.py | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/boa/unitary/pool/stateful/stateful_base.py b/tests/boa/unitary/pool/stateful/stateful_base.py index 2e00d525..5b5cc512 100644 --- a/tests/boa/unitary/pool/stateful/stateful_base.py +++ b/tests/boa/unitary/pool/stateful/stateful_base.py @@ -221,15 +221,9 @@ def balances(self): balances = [self.swap.balances(i) for i in range(3)] balances_of = [c.balanceOf(self.swap) for c in self.coins] - stored_balances = [c.internal.stored_balances(c) for c in self.coins] for i in range(3): - assert ( - self.balances[i] - == balances[i] - == balances_of[i] - == stored_balances[i] - ) + assert self.balances[i] == balances[i] == balances_of[i] @invariant() def lp_token_total_supply(self): 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 8222e7f3..de8db7fe 100644 --- a/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py +++ b/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py @@ -54,6 +54,16 @@ def exchange(self, exchange_amount_in, exchange_i, exchange_j, user): exchange_amount_in_converted, exchange_i, exchange_j, user ) + @rule( + exchange_amount_in=exchange_amount_in, + exchange_i=exchange_i, + exchange_j=exchange_j, + user=user, + ) + def add_liquidity(self, exchange_amount_in, exchange_i, exchange_j, user): + + raise + def test_admin_fee(swap, views_contract, users, pool_coins, tricrypto_factory): from hypothesis import settings diff --git a/tests/boa/unitary/pool/stateful/test_stateful.py b/tests/boa/unitary/pool/stateful/test_stateful.py index f015a72d..c4fe863e 100644 --- a/tests/boa/unitary/pool/stateful/test_stateful.py +++ b/tests/boa/unitary/pool/stateful/test_stateful.py @@ -103,7 +103,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 From 8d1b60962390d23a880da1bf66377519a676f36a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Sun, 6 Aug 2023 15:06:42 +0200 Subject: [PATCH 20/53] fix: fee was waaaayyyy too high --- contracts/main/CurveTricryptoOptimizedWETH.vy | 2 +- tests/boa/unitary/pool/stateful/stateful_base.py | 5 +---- tests/boa/unitary/pool/stateful/test_simulate.py | 1 - tests/boa/unitary/pool/test_deposit_withdraw.py | 8 ++++---- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 9d248949..87e478e5 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1204,7 +1204,7 @@ def _fee(xp: uint256[N_COINS]) -> uint256: fee_params: uint256[3] = self._unpack(self.packed_fee_params) - if self.future_A_gamma_time < block.timestamp: + if self.future_A_gamma_time > block.timestamp: # TODO: do not charge max fee else pool rekt! fee_params[0] = MAX_FEE # mid_fee is MAX_FEE during ramping fee_params[1] = MAX_FEE # out_fee is MAX_FEE during ramping diff --git a/tests/boa/unitary/pool/stateful/stateful_base.py b/tests/boa/unitary/pool/stateful/stateful_base.py index 5b5cc512..ec195678 100644 --- a/tests/boa/unitary/pool/stateful/stateful_base.py +++ b/tests/boa/unitary/pool/stateful/stateful_base.py @@ -63,8 +63,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 +161,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 +177,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 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/test_deposit_withdraw.py b/tests/boa/unitary/pool/test_deposit_withdraw.py index e06be852..458e30dc 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] From 7acfc141786e5d2dafefd46ef49beb9b9c3d848c Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 7 Aug 2023 19:01:46 +0200 Subject: [PATCH 21/53] update tests and fix admin fee claiming logic --- contracts/main/CurveTricryptoOptimizedWETH.vy | 16 +++---- .../unitary/pool/stateful/stateful_base.py | 43 ++++++++++++++++--- .../pool/stateful/test_admin_fee_claim.py | 28 +++++++++--- .../pool/stateful/test_gas_realistic.py | 9 +++- tests/boa/unitary/pool/stateful/test_ramp.py | 13 ++++++ .../pool/stateful/test_ramp_nocheck.py | 13 ++++++ .../unitary/pool/stateful/test_stateful.py | 8 +++- 7 files changed, 107 insertions(+), 23 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 87e478e5..8e87d39c 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1060,10 +1060,9 @@ def _claim_admin_fees(): # 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 + block.timestamp - last_claim_time < MIN_ADMIN_FEE_CLAIM_INTERVAL or + self.future_A_gamma_time > block.timestamp ): return @@ -1079,7 +1078,7 @@ def _claim_admin_fees(): # 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 current_lp_token_supply < 10**18): + if xcp_profit <= xcp_profit_a or current_lp_token_supply < 10**18: return # ---------- Conditions met to claim admin fees: compute state. ---------- @@ -1203,12 +1202,13 @@ def _A_gamma() -> uint256[2]: 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]) - if self.future_A_gamma_time > block.timestamp: # TODO: do not charge max fee else pool rekt! - fee_params[0] = MAX_FEE # mid_fee is MAX_FEE during ramping - fee_params[1] = MAX_FEE # out_fee is MAX_FEE during ramping + # During parameter ramping, we raise fees and disable admin fee claiming + if self.future_A_gamma_time > block.timestamp: # parameter ramping + fee_params[0] = 10**8 # set mid_fee to 100 basis points + fee_params[1] = 10**8 # set out_fee to 100 basis points - f: uint256 = MATH.reduction_coefficient(xp, fee_params[2]) return unsafe_div( fee_params[0] * f + fee_params[1] * (10**18 - f), 10**18 diff --git a/tests/boa/unitary/pool/stateful/stateful_base.py b/tests/boa/unitary/pool/stateful/stateful_base.py index ec195678..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 @@ -259,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 de8db7fe..8dc06853 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 @@ -16,10 +17,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") @@ -54,15 +61,24 @@ def exchange(self, exchange_amount_in, exchange_i, exchange_j, user): exchange_amount_in_converted, exchange_i, exchange_j, user ) + @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( - exchange_amount_in=exchange_amount_in, + token_amount=token_amount, exchange_i=exchange_i, - exchange_j=exchange_j, user=user, ) - def add_liquidity(self, exchange_amount_in, exchange_i, exchange_j, user): + def remove_liquidity_one_coin(self, token_amount, exchange_i, user): - raise + 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 2e494abf..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: diff --git a/tests/boa/unitary/pool/stateful/test_ramp.py b/tests/boa/unitary/pool/stateful/test_ramp.py index 69af1f0c..1340b9d5 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]] + [ @@ -99,6 +101,17 @@ def virtual_price(self): # Invariant is not conserved here pass + @invariant() + def check_bumped_fee_during_ramp(self): + if self.swap.future_A_gamma_time() > boa.env.vm.state.timestamp: + assert ( + self.swap.fee() >= 10**8 + ) # Charge at least 100 basis points! + + @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..14c5a657 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,17 @@ def up_only_profit(self): # so we need to override super().up_only_profit() pass + @invariant() + def check_bumped_fee_during_ramp(self): + if self.swap.future_A_gamma_time() > boa.env.vm.state.timestamp: + assert ( + self.swap.fee() >= 10**8 + ) # Charge at least 100 basis points! + + @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_stateful.py b/tests/boa/unitary/pool/stateful/test_stateful.py index c4fe863e..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: From 96a09b9b69c5f65bac94fb456e45f4166ed3b42a Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 8 Aug 2023 06:32:29 +0200 Subject: [PATCH 22/53] add tests for exchange_received --- tests/boa/unitary/pool/test_exchange.py | 85 +++++++++++++++++++ .../unitary/pool/test_exchange_received.py | 1 - 2 files changed, 85 insertions(+), 1 deletion(-) delete mode 100644 tests/boa/unitary/pool/test_exchange_received.py diff --git a/tests/boa/unitary/pool/test_exchange.py b/tests/boa/unitary/pool/test_exchange.py index 3252d239..f7906aa5 100644 --- a/tests/boa/unitary/pool/test_exchange.py +++ b/tests/boa/unitary/pool/test_exchange.py @@ -27,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) @@ -34,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) @@ -57,3 +59,86 @@ def test_exchange_all( assert d_balance_i == amount assert -d_balance_j == measured_j + + +@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_received_success( + swap_with_deposit, + views_contract, + coins, + user, + amount, + i, + j, +): + + 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(i, j, amount, swap_with_deposit) + + measured_i = coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) + d_balance_i = swap_with_deposit.balances(i) + d_balance_j = swap_with_deposit.balances(j) + + with boa.env.prank(user): + coins[i].transfer(swap_with_deposit, amount) + swap_with_deposit.exchange_received( + i, j, amount, int(0.999 * calculated), user + ) + + measured_i -= coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) - measured_j + d_balance_i = swap_with_deposit.balances(i) - d_balance_i + d_balance_j = swap_with_deposit.balances(j) - d_balance_j + + assert amount == measured_i + assert calculated == measured_j + + assert d_balance_i == amount + assert -d_balance_j == measured_j + + +@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_received_revert_on_no_transfer( + swap_with_deposit, + views_contract, + coins, + user, + amount, + i, + j, +): + + 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(i, j, amount, swap_with_deposit) + + with boa.env.prank(user), boa.reverts(dev="user didn't give us coins"): + swap_with_deposit.exchange_received( + i, j, amount, int(0.999 * calculated), user + ) diff --git a/tests/boa/unitary/pool/test_exchange_received.py b/tests/boa/unitary/pool/test_exchange_received.py deleted file mode 100644 index 3e2edc91..00000000 --- a/tests/boa/unitary/pool/test_exchange_received.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Add test exchange_received From 5b6bc95bc2895c631deb04dab4028fba4fe7d761 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 8 Aug 2023 07:16:06 +0200 Subject: [PATCH 23/53] test donation does not account in self.balances; add comments explaining the logic --- contracts/main/CurveTricryptoOptimizedWETH.vy | 6 +++ tests/boa/unitary/pool/test_exchange.py | 50 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 8e87d39c..2c7bb329 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -340,6 +340,12 @@ def _transfer_in( ) received_amounts = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance + # 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. assert received_amounts >= dx # dev: user didn't give us coins self.balances[_coin_idx] += dx diff --git a/tests/boa/unitary/pool/test_exchange.py b/tests/boa/unitary/pool/test_exchange.py index f7906aa5..3452bfb9 100644 --- a/tests/boa/unitary/pool/test_exchange.py +++ b/tests/boa/unitary/pool/test_exchange.py @@ -111,6 +111,56 @@ def test_exchange_received_success( assert -d_balance_j == measured_j +@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_received_send_extra( + swap_with_deposit, + views_contract, + coins, + user, + amount, + i, + j, +): + + if i == j or i > 2 or j > 2: + + return + + amount = amount * 10**18 // INITIAL_PRICES[i] + mint_for_testing(coins[i], user, amount + 1) # <--- mint 1 wei extra + + calculated = views_contract.get_dy(i, j, amount, swap_with_deposit) + + measured_i = coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) + d_balance_i = swap_with_deposit.balances(i) + d_balance_j = swap_with_deposit.balances(j) + + with boa.env.prank(user): + coins[i].transfer(swap_with_deposit, amount + 1) # <--- send extra + swap_with_deposit.exchange_received( + i, j, amount, int(0.999 * calculated), user + ) + + measured_i -= coins[i].balanceOf(user) + measured_j = coins[j].balanceOf(user) - measured_j + d_balance_i = swap_with_deposit.balances(i) - d_balance_i + d_balance_j = swap_with_deposit.balances(j) - d_balance_j + + assert amount == measured_i - 1 # <--- we sent 1 wei extra + assert calculated == measured_j + + assert d_balance_i == amount # <--- we sent 1 wei extra + assert -d_balance_j == measured_j + + @given( amount=strategy( "uint256", min_value=10**10, max_value=2 * 10**6 * 10**18 From 958ffd5c1e433e0ad2aaa363b47b462bbf8a5dfe Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:04:11 +0200 Subject: [PATCH 24/53] fix: must adjust virtual price and D anyway --- contracts/main/CurveTricryptoOptimizedWETH.vy | 8 +++++ .../boa/unitary/pool/test_deposit_withdraw.py | 29 ++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 2c7bb329..da7272c1 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1154,6 +1154,14 @@ def _claim_admin_fees(): self.xcp_profit = xcp_profit self.last_admin_fee_claim_timestamp = block.timestamp + # Since we reduce balances: virtual price goes down + self.virtual_price = vprice + + # The _claim_admin_fee is operationally similar to: + # Calculate admin's share of fees > mint LP tokens > admin does remove_liquidity + # So: 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. diff --git a/tests/boa/unitary/pool/test_deposit_withdraw.py b/tests/boa/unitary/pool/test_deposit_withdraw.py index 458e30dc..cfb94d18 100644 --- a/tests/boa/unitary/pool/test_deposit_withdraw.py +++ b/tests/boa/unitary/pool/test_deposit_withdraw.py @@ -154,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 @@ -204,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 @@ -315,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 @@ -344,10 +364,11 @@ 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 From 7a21ea57b9f87ef2a02f97665211a4533a49c4f0 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:05:30 +0200 Subject: [PATCH 25/53] remove TODO in contract --- contracts/main/CurveTricryptoOptimizedWETH.vy | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index da7272c1..265e8ce7 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1054,8 +1054,6 @@ def tweak_price( def _claim_admin_fees(): """ @notice Claims admin fees and sends it to fee_receiver set in the factory. - # TODO: test if this breaks the AMM! We're not minting LP tokens for - # the admin """ # --------------------- Check if fees can be claimed --------------------- From e55ae53efb98914956bf058f4cb13b860cd361b2 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:23:32 +0200 Subject: [PATCH 26/53] fix test_get_p test --- tests/boa/unitary/math/test_get_p.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/tests/boa/unitary/math/test_get_p.py b/tests/boa/unitary/math/test_get_p.py index 538b2fde..b587241d 100644 --- a/tests/boa/unitary/math/test_get_p.py +++ b/tests/boa/unitary/math/test_get_p.py @@ -52,17 +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() - - balances = [] - for i in range(3): - balances.append(swap.balances(i)) - xp = swap.internal.xp( - balances, swap._storage.price_scale_packed.get(), swap.precisions() + swap._storage.balances.get(), + swap._storage.price_scale_packed.get(), + swap.precisions(), ) for k in range(3): @@ -73,10 +66,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): From 7467292a5fa913382bfde343bbef0e62dcc523b1 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 9 Aug 2023 17:57:26 +0200 Subject: [PATCH 27/53] cei --- contracts/main/CurveTricryptoOptimizedWETH.vy | 150 ++++++++++-------- 1 file changed, 87 insertions(+), 63 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 265e8ce7..63dbe417 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -141,7 +141,7 @@ packed_precisions: immutable(uint256) 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. @@ -248,8 +248,7 @@ def __init__( WETH20 = _weth MATH = Math(_math) - self.factory = msg.sender - + factory = Factory(msg.sender) name = _name symbol = _symbol coins = _coins @@ -325,13 +324,22 @@ def _transfer_in( """ received_amounts: uint256 = 0 coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) + recorded_balance: uint256 = self.balances[_coin_idx] + + # Adjust balances before handling transfers + self.balances[_coin_idx] += dx + # Handle transfers: if expect_optimistic_transfer: - received_amounts = coin_balance - self.balances[_coin_idx] + # Only enabled in exchange_received: it expects the caller + # of exchange_received to have sent tokens to the pool before + # calling this method. + received_amounts = coin_balance - recorded_balance else: + # EXTERNAL CALL assert ERC20(coins[_coin_idx]).transferFrom( sender, self, @@ -347,7 +355,6 @@ def _transfer_in( # If we checked for received_amounts == dx, an extra transfer without a # call to exchange_received will break the method. assert received_amounts >= dx # dev: user didn't give us coins - self.balances[_coin_idx] += dx @internal @@ -360,12 +367,16 @@ def _transfer_out(_coin_idx: uint256, _amount: uint256, receiver: address): @params _amount Amount of token to transfer out @params receiver Address to send the tokens to """ + + # Adjust balances before handling transfers: + self.balances[_coin_idx] -= _amount + + # EXTERNAL CALL assert ERC20(coins[_coin_idx]).transfer( receiver, _amount, default_return_value=True ) - self.balances[_coin_idx] -= _amount # -------------------------- AMM Main Functions ------------------------------ @@ -481,20 +492,9 @@ def add_liquidity( PRECISION ) - # ---------------- transferFrom token into the pool ---------------------- - + # recalc amountsp: for i in range(N_COINS): - if amounts[i] > 0: - - # Updates self.balances after checking transferred in amounts: - self._transfer_in( - i, - amounts[i], - msg.sender, - False, # <--------------------- Disable optimistic transfers. - ) - amountsp[i] = xp[i] - xp_old[i] # -------------------- Calculate LP tokens to mint ----------------------- @@ -541,11 +541,26 @@ def add_liquidity( assert d_token >= min_mint_amount, "Slippage" + # ---------------- transferFrom token into the pool ---------------------- + + for i in range(N_COINS): + + if amounts[i] > 0: + + # _transfer_out updates self.balances here. Update to state occurs + # before external calls: + self._transfer_in( + i, + amounts[i], + msg.sender, + False, # <--------------------- Disable optimistic transfers. + ) + log AddLiquidity( receiver, amounts, 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 @@ -607,7 +622,8 @@ def remove_liquidity( # ---------------------------------- Transfers --------------------------- for i in range(N_COINS): - # _transfer_out updates self.balances: + # _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, withdraw_amounts, total_supply - _amount) @@ -653,22 +669,25 @@ def remove_liquidity_one_coin( assert dy >= min_amount, "Slippage" - # ------------------------- Transfers ------------------------------------ + # ---------------------------- State Updates ----------------------------- # Burn user's tokens: self.burnFrom(msg.sender, token_amount) - # _transfer_out updates self.balances: - self._transfer_out(i, dy, 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 ) - self._claim_admin_fees() # <--------------------------- Claim admin fees. + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. return dy @@ -677,7 +696,7 @@ def remove_liquidity_one_coin( @internal -@view +@pure def _pack(x: uint256[3]) -> uint256: """ @notice Packs 3 integers with values <= 10**18 into a uint256 @@ -688,7 +707,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) @@ -703,7 +722,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. @@ -721,7 +740,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. @@ -817,11 +836,16 @@ def _exchange( y = unsafe_div(y * price_scale[j - 1], PRECISION) xp[j] = y # <------------------------------------------------- Update xp. + # ------ Tweak price_scale with good initial guess for newton_D ---------- + + packed_price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1]) + # ---------------------- Do Transfers in and out ------------------------- ########################## TRANSFER IN <------- - # _transfer_in updates self.balances here: + # _transfer_in updates self.balances here. Update to state occurs before + # external calls: self._transfer_in( i, dx, @@ -831,13 +855,10 @@ def _exchange( ########################## -------> TRANSFER OUT - # _transfer_out updates self.balances here: + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: self._transfer_out(j, dy, receiver) - # ------ Tweak price_scale with good initial guess for newton_D ---------- - - packed_price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1]) - log TokenExchange(sender, i, dx, j, dy, fee, packed_price_scale) return dy @@ -1054,6 +1075,10 @@ 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 --------------------- @@ -1090,7 +1115,7 @@ def _claim_admin_fees(): vprice: uint256 = self.virtual_price packed_price_scale: uint256 = self.price_scale_packed precisions: uint256[N_COINS] = self._unpack(packed_precisions) - fee_receiver: address = Factory(self.factory).fee_receiver() + fee_receiver: address = factory.fee_receiver() balances: uint256[N_COINS] = self.balances # Admin fees are calculated as follows. @@ -1134,9 +1159,23 @@ def _claim_admin_fees(): # ---------------------------- Update State ------------------------------ + self.xcp_profit = xcp_profit + self.last_admin_fee_claim_timestamp = block.timestamp + + # Since we reduce balances: virtual price goes down + self.virtual_price = vprice + + # 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. + + # --------------------------- Handle Transfers --------------------------- + + admin_tokens: uint256[N_COINS] = empty(uint256[N_COINS]) if admin_share > 0: - admin_tokens: uint256[N_COINS] = empty(uint256[N_COINS]) for i in range(N_COINS): admin_tokens[i] = ( @@ -1144,25 +1183,12 @@ def _claim_admin_fees(): total_supply_including_admin_share ) - # _transfer_out tokens to admin and update self.balances: + # _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) - self.xcp_profit = xcp_profit - self.last_admin_fee_claim_timestamp = block.timestamp - - # Since we reduce balances: virtual price goes down - self.virtual_price = vprice - - # The _claim_admin_fee is operationally similar to: - # Calculate admin's share of fees > mint LP tokens > admin does remove_liquidity - # So: 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 @pure @@ -1560,7 +1586,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 @@ -1574,7 +1600,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) @@ -1589,7 +1615,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) @@ -1607,7 +1633,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) @@ -1621,9 +1647,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 @@ -1893,7 +1917,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 @@ -1938,7 +1962,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 @@ -1972,7 +1996,7 @@ def commit_new_parameters( @param _new_adjustment_step The new adjustment step. @param _new_ma_time The new ma time. ma_time is time_in_seconds/ln(2). """ - assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert msg.sender == factory.admin() # dev: only owner assert self.admin_actions_deadline == 0 # dev: active action _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY @@ -2078,5 +2102,5 @@ def revert_new_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 + assert msg.sender == factory.admin() # dev: only owner self.admin_actions_deadline = 0 From 10507c9e64b454937ab806268a7ce657b5f36366 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:13:27 +0200 Subject: [PATCH 28/53] set default receiver in exchange received to msg.sender --- contracts/main/CurveTricryptoOptimizedWETH.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 63dbe417..699d0296 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -418,7 +418,7 @@ def exchange_received( j: uint256, dx: uint256, min_dy: uint256, - receiver: address, + receiver: address = msg.sender, ) -> uint256: """ @notice Exchange: but user must transfer dx amount of coin[i] tokens to pool first From 178d8e455c4baa4bc6dfc379decd69b6c172f2f6 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:40:51 +0200 Subject: [PATCH 29/53] add getter for gauge_implementaiton in l2 factory that points to zero --- contracts/main/CurveL2TricryptoFactory.vy | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/main/CurveL2TricryptoFactory.vy b/contracts/main/CurveL2TricryptoFactory.vy index 3ef0f8e4..a61e9f64 100644 --- a/contracts/main/CurveL2TricryptoFactory.vy +++ b/contracts/main/CurveL2TricryptoFactory.vy @@ -381,6 +381,17 @@ def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address # <--- Pool Getters ---> +@view +@external +def gauge_implementation() -> address: + """ + @notic returns gauge implementation address stored in the factory + @dev Gauges are not deployed by non-eth AMM factories. + @return Empty address + """ + return empty(address) + + @view @external def get_coins(_pool: address) -> address[N_COINS]: From 043705ae3465d7454447147187221ba27e3b0c80 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:53:48 +0200 Subject: [PATCH 30/53] revert changes to L2 factory --- contracts/main/CurveL2TricryptoFactory.vy | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/contracts/main/CurveL2TricryptoFactory.vy b/contracts/main/CurveL2TricryptoFactory.vy index a61e9f64..3ef0f8e4 100644 --- a/contracts/main/CurveL2TricryptoFactory.vy +++ b/contracts/main/CurveL2TricryptoFactory.vy @@ -381,17 +381,6 @@ def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address # <--- Pool Getters ---> -@view -@external -def gauge_implementation() -> address: - """ - @notic returns gauge implementation address stored in the factory - @dev Gauges are not deployed by non-eth AMM factories. - @return Empty address - """ - return empty(address) - - @view @external def get_coins(_pool: address) -> address[N_COINS]: From 9eab58c74639eb16c0c78dc7d99ca224feda0e36 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:12:54 +0200 Subject: [PATCH 31/53] add tvl oracle --- contracts/main/CurveTricryptoOptimizedWETH.vy | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 699d0296..0b055fb6 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -145,9 +145,11 @@ factory: public(immutable(Factory)) price_scale_packed: uint256 # <------------------------ Internal price scale. price_oracle_packed: uint256 # <------- Price target given by moving average. +tvl_oracle: uint256 # <------------------ EMA of totalSupply * virtual_price. last_prices_packed: uint256 last_prices_timestamp: public(uint256) +last_tvl: public(uint256) initial_A_gamma: public(uint256) initial_A_gamma_time: public(uint256) @@ -903,11 +905,14 @@ def tweak_price( old_xcp_profit: uint256 = self.xcp_profit old_virtual_price: uint256 = self.virtual_price last_prices_timestamp: uint256 = self.last_prices_timestamp + last_cached_tvl: uint256 = self.last_tvl # ----------------------- Update MA if needed ---------------------------- if last_prices_timestamp < block.timestamp: + tvl_oracle: uint256 = self.tvl_oracle + # The moving average price oracle is calculated using the last_price # of the trade at the previous block, and the price oracle logged # before that trade. This can happen only once per block. @@ -924,6 +929,8 @@ def tweak_price( ) ) + # ---------------------------------------------- Update price oracles. + for k in range(N_COINS - 1): # ----------------- We cap state price that goes into the EMA with @@ -934,6 +941,14 @@ def tweak_price( 10**18 ) + # ------------------------------------------------- Update TVL oracle. + + tvl_oracle = unsafe_div( + last_cached_tvl * (10**18 - alpha) + tvl_oracle * alpha, + 10**18 + ) + + self.tvl_oracle = tvl_oracle self.price_oracle_packed = self._pack_prices(price_oracle) self.last_prices_timestamp = block.timestamp # <---- Store timestamp. @@ -954,6 +969,10 @@ def tweak_price( last_prices[k] = unsafe_div(last_prices[k] * price_scale[k], 10**18) self.last_prices_packed = self._pack_prices(last_prices) + # -------------------------- Calculate last_tvl -------------------------- + + self.last_tvl = unsafe_div(total_supply * old_virtual_price, 10**18) + # ---------- Update profit numbers without price adjustment first -------- xp: uint256[N_COINS] = empty(uint256[N_COINS]) From ae3e5cd48b7ff64ec2ba9f488812e7633ee63021 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 14 Aug 2023 13:42:27 +0200 Subject: [PATCH 32/53] update tvl oracle --- contracts/main/CurveTricryptoOptimizedWETH.vy | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 0b055fb6..49b2011a 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -145,7 +145,7 @@ factory: public(immutable(Factory)) price_scale_packed: uint256 # <------------------------ Internal price scale. price_oracle_packed: uint256 # <------- Price target given by moving average. -tvl_oracle: uint256 # <------------------ EMA of totalSupply * virtual_price. +cached_tvl_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. last_prices_packed: uint256 last_prices_timestamp: public(uint256) @@ -905,13 +905,13 @@ def tweak_price( old_xcp_profit: uint256 = self.xcp_profit old_virtual_price: uint256 = self.virtual_price last_prices_timestamp: uint256 = self.last_prices_timestamp - last_cached_tvl: uint256 = self.last_tvl - # ----------------------- Update MA if needed ---------------------------- + # ----------------------- Update Oracles if needed ----------------------- if last_prices_timestamp < block.timestamp: - tvl_oracle: uint256 = self.tvl_oracle + cached_tvl_oracle: uint256 = self.cached_tvl_oracle + last_cached_tvl: uint256 = self.last_tvl # The moving average price oracle is calculated using the last_price # of the trade at the previous block, and the price oracle logged @@ -941,20 +941,23 @@ def tweak_price( 10**18 ) + self.price_oracle_packed = self._pack_prices(price_oracle) + # ------------------------------------------------- Update TVL oracle. - tvl_oracle = unsafe_div( - last_cached_tvl * (10**18 - alpha) + tvl_oracle * alpha, + self.cached_tvl_oracle = unsafe_div( + last_cached_tvl * (10**18 - alpha) + cached_tvl_oracle * alpha, 10**18 ) - self.tvl_oracle = tvl_oracle - 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. + # `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 ------------------ @@ -1726,6 +1729,39 @@ def price_oracle(k: uint256) -> uint256: return price_oracle +@external +@view +@nonreentrant("lock") +def tvl_oracle() -> uint256: + """ + @notice Returns a tvl oracle. + @dev The oracle is an exponential moving average, with a periodicity + determined by `self.ma_time`. Input to the TVL oracle is totalSupply * virtual_price + @return uint256 Oracle value of TVL. + """ + + last_prices_timestamp: uint256 = self.last_prices_timestamp + cached_tvl_oracle: uint256 = self.cached_tvl_oracle + + if last_prices_timestamp < block.timestamp: + + ma_time: uint256 = self._unpack(self.packed_rebalancing_params)[2] + alpha: uint256 = MATH.wad_exp( + -convert( + (block.timestamp - last_prices_timestamp) * 10**18 / ma_time, + int256, + ) + ) + + total_supply: uint256 = self.totalSupply + virtual_price: uint256 = 10**18 * self.get_xcp(self.D, self.price_scale_packed) / total_supply + last_tvl: uint256 = total_supply * virtual_price + + return (last_tvl * (10**18 - alpha) + cached_tvl_oracle * alpha) / 10**18 + + return cached_tvl_oracle + + @external @view def last_prices(k: uint256) -> uint256: From 73b5224ba876b01803e60345ac02689b83a2e1ec Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:25:50 +0200 Subject: [PATCH 33/53] docstrings --- contracts/main/CurveTricryptoOptimizedWETH.vy | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 49b2011a..e158dda8 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -3,7 +3,7 @@ """ @title CurveTricryptoOptimizedWETH @author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@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. """ @@ -309,20 +309,11 @@ def _transfer_in( """ @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 expect_optimistic_transfer bool True if pool expects user to transfer. + This is only enabled for exchange_received. """ received_amounts: uint256 = 0 coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) @@ -364,8 +355,8 @@ 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 receiver Address to send the tokens to """ @@ -423,16 +414,16 @@ def exchange_received( receiver: address = msg.sender, ) -> uint256: """ - @notice Exchange: but user must transfer dx amount of coin[i] tokens to pool first + @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. + transfers in zaps. Primarily for dex-aggregators/arbitrageurs/searchers. @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 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` """ return self._exchange( @@ -874,7 +865,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. From 52ae0704d3bac42fad823d2e1640edc50b9eb053 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 23 Aug 2023 11:48:45 +0200 Subject: [PATCH 34/53] optimise D oracle and rename to match stableswap-ng --- contracts/main/CurveTricryptoOptimizedWETH.vy | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index e158dda8..74faa132 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -963,10 +963,6 @@ def tweak_price( last_prices[k] = unsafe_div(last_prices[k] * price_scale[k], 10**18) self.last_prices_packed = self._pack_prices(last_prices) - # -------------------------- Calculate last_tvl -------------------------- - - self.last_tvl = unsafe_div(total_supply * old_virtual_price, 10**18) - # ---------- Update profit numbers without price adjustment first -------- xp: uint256[N_COINS] = empty(uint256[N_COINS]) @@ -995,6 +991,10 @@ def tweak_price( if self.future_A_gamma_time < block.timestamp: assert virtual_price > old_virtual_price, "Loss" + # -------------------------- Cache last_tvl -------------------------- + + self.last_tvl = xcp # geometric_mean(D * price_scale) + self.xcp_profit = xcp_profit # ------------ Rebalance liquidity if there's enough profits to adjust it: @@ -1723,7 +1723,7 @@ def price_oracle(k: uint256) -> uint256: @external @view @nonreentrant("lock") -def tvl_oracle() -> uint256: +def D_oracle() -> uint256: """ @notice Returns a tvl oracle. @dev The oracle is an exponential moving average, with a periodicity @@ -1744,10 +1744,7 @@ def tvl_oracle() -> uint256: ) ) - total_supply: uint256 = self.totalSupply - virtual_price: uint256 = 10**18 * self.get_xcp(self.D, self.price_scale_packed) / total_supply - last_tvl: uint256 = total_supply * virtual_price - + last_tvl: uint256 = self.get_xcp(self.D, self.price_scale_packed) return (last_tvl * (10**18 - alpha) + cached_tvl_oracle * alpha) / 10**18 return cached_tvl_oracle From 29940dfb53be1febe806bd687ef1bae127463f1b Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:48:08 +0200 Subject: [PATCH 35/53] add tests; change name to xcp_oracle --- contracts/main/CurveTricryptoOptimizedWETH.vy | 44 ++++++----- .../pool/stateful/test_admin_fee_claim.py | 2 - tests/boa/unitary/pool/test_a_gamma.py | 4 - tests/boa/unitary/pool/test_oracles.py | 76 +++++++++++++++++++ 4 files changed, 103 insertions(+), 23 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 74faa132..2908a93c 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -145,11 +145,11 @@ factory: public(immutable(Factory)) price_scale_packed: uint256 # <------------------------ Internal price scale. price_oracle_packed: uint256 # <------- Price target given by moving average. -cached_tvl_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. +cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. last_prices_packed: uint256 last_prices_timestamp: public(uint256) -last_tvl: public(uint256) +last_xcp: public(uint256) initial_A_gamma: public(uint256) initial_A_gamma_time: public(uint256) @@ -526,10 +526,16 @@ 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 + self.mint(receiver, d_token) assert d_token >= min_mint_amount, "Slippage" @@ -901,9 +907,6 @@ def tweak_price( if last_prices_timestamp < block.timestamp: - cached_tvl_oracle: uint256 = self.cached_tvl_oracle - last_cached_tvl: uint256 = self.last_tvl - # The moving average price oracle is calculated using the last_price # of the trade at the previous block, and the price oracle logged # before that trade. This can happen only once per block. @@ -936,8 +939,11 @@ def tweak_price( # ------------------------------------------------- Update TVL oracle. - self.cached_tvl_oracle = unsafe_div( - last_cached_tvl * (10**18 - alpha) + cached_tvl_oracle * alpha, + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + last_cached_tvl: uint256 = self.last_xcp + + self.cached_xcp_oracle = unsafe_div( + last_cached_tvl * (10**18 - alpha) + cached_xcp_oracle * alpha, 10**18 ) @@ -991,9 +997,9 @@ def tweak_price( if self.future_A_gamma_time < block.timestamp: assert virtual_price > old_virtual_price, "Loss" - # -------------------------- Cache last_tvl -------------------------- + # -------------------------- Cache last_xcp -------------------------- - self.last_tvl = xcp # geometric_mean(D * price_scale) + self.last_xcp = xcp # geometric_mean(D * price_scale) self.xcp_profit = xcp_profit @@ -1723,16 +1729,20 @@ def price_oracle(k: uint256) -> uint256: @external @view @nonreentrant("lock") -def D_oracle() -> uint256: +def xcp_oracle() -> uint256: """ - @notice Returns a tvl oracle. + @notice Returns the oracle value for xcp. @dev The oracle is an exponential moving average, with a periodicity - determined by `self.ma_time`. Input to the TVL oracle is totalSupply * virtual_price - @return uint256 Oracle value of TVL. + determined by `self.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_tvl_oracle: uint256 = self.cached_tvl_oracle + cached_xcp_oracle: uint256 = self.cached_xcp_oracle if last_prices_timestamp < block.timestamp: @@ -1744,10 +1754,10 @@ def D_oracle() -> uint256: ) ) - last_tvl: uint256 = self.get_xcp(self.D, self.price_scale_packed) - return (last_tvl * (10**18 - alpha) + cached_tvl_oracle * alpha) / 10**18 + last_xcp: uint256 = self.get_xcp(self.D, self.price_scale_packed) + return (last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha) / 10**18 - return cached_tvl_oracle + return cached_xcp_oracle @external 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 8dc06853..8064e056 100644 --- a/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py +++ b/tests/boa/unitary/pool/stateful/test_admin_fee_claim.py @@ -10,8 +10,6 @@ STEP_COUNT = 100 NO_CHANGE = 2**256 - 1 -# TODO: Test admin fee claims considering the various cases where it is disallowed. # noqa: E501 - def approx(x1, x2, precision): return abs(log(x1 / x2)) <= precision diff --git a/tests/boa/unitary/pool/test_a_gamma.py b/tests/boa/unitary/pool/test_a_gamma.py index 7e09cf0a..715b7678 100644 --- a/tests/boa/unitary/pool/test_a_gamma.py +++ b/tests/boa/unitary/pool/test_a_gamma.py @@ -48,7 +48,3 @@ def test_ramp_A_gamma(swap, factory_admin): ) < 1e-4 * A_gamma_initial[1] ) - - -# TODO: Add check for fees during ramps -# TODO: Add test to ensure admin fees are not being claimed diff --git a/tests/boa/unitary/pool/test_oracles.py b/tests/boa/unitary/pool/test_oracles.py index 89836d9c..9b3f7e92 100644 --- a/tests/boa/unitary/pool/test_oracles.py +++ b/tests/boa/unitary/pool/test_oracles.py @@ -26,6 +26,20 @@ def norm(price_oracle, price_scale): return sqrt(norm) +def get_D_oracle_input(swap, math_contract): + + A, gamma = swap.internal._A_gamma() + xp = swap.internal.xp( + swap._storage.balances.get(), + swap._storage.price_scale_packed.get(), + swap.internal._unpack(swap._immutables.packed_precisions), + ) + D = math_contract.newton_D(A, gamma, xp) + xcp = math_contract.geometric_mean(xp) + + return D, xcp + + def test_initial(swap_with_deposit): for i in range(2): assert swap_with_deposit.price_scale(i) == INITIAL_PRICES[i + 1] @@ -95,6 +109,68 @@ 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) + + rebal_params = swap_with_deposit.internal._unpack( + swap_with_deposit._storage.packed_rebalancing_params.get() + ) + ma_time = rebal_params[2] + + # 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( From e6d616ca3d73d9215bd2a65277e51725f47e2516 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Sun, 27 Aug 2023 16:18:12 +0200 Subject: [PATCH 36/53] add clarity in comment --- contracts/main/CurveTricryptoOptimizedWETH.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 2908a93c..02d501d2 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -534,7 +534,7 @@ def add_liquidity( self.xcp_profit_a = 10**18 # Initialise xcp oracle here: - self.cached_xcp_oracle = d_token # <--- virtual_price * totalSupply + self.cached_xcp_oracle = d_token # <--- virtual_price * totalSupply / 10**18 self.mint(receiver, d_token) From 2eea2a5d1fdf3e9cbb655e4d576d63e9d464931e Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:55:59 +0200 Subject: [PATCH 37/53] fix: precisions is not longer stored packed --- contracts/main/CurveTricryptoOptimizedWETH.vy | 46 +++++++------------ tests/boa/unitary/math/test_get_p.py | 1 - tests/boa/unitary/math/test_get_p_expt.py | 4 +- tests/boa/unitary/pool/test_oracles.py | 1 - 4 files changed, 18 insertions(+), 34 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 02d501d2..fb1c8272 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -137,7 +137,7 @@ 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: immutable(uint256) +PRECISIONS: immutable(uint256[N_COINS]) MATH: public(immutable(Math)) coins: public(immutable(address[N_COINS])) @@ -255,8 +255,8 @@ def __init__( symbol = _symbol coins = _coins - 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 @@ -464,7 +464,6 @@ def add_liquidity( # --------------------- Get prices, balances ----------------------------- - precisions: uint256[N_COINS] = self._unpack(packed_precisions) packed_price_scale: uint256 = self.price_scale_packed price_scale: uint256[N_COINS-1] = self._unpack_prices(packed_price_scale) @@ -476,12 +475,12 @@ def add_liquidity( xx = xp - xp[0] *= precisions[0] - xp_old[0] *= precisions[0] + 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[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]), + xp_old[i] * unsafe_mul(price_scale[i-1], PRECISIONS[i]), PRECISION ) @@ -774,7 +773,6 @@ def _exchange( A_gamma: uint256[2] = self._A_gamma() xp: uint256[N_COINS] = self.balances - precisions: uint256[N_COINS] = self._unpack(packed_precisions) dy: uint256 = 0 y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert. @@ -786,14 +784,14 @@ def _exchange( 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 --------- @@ -813,7 +811,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 @@ -1133,7 +1131,6 @@ def _claim_admin_fees(): vprice: uint256 = self.virtual_price packed_price_scale: uint256 = self.price_scale_packed - precisions: uint256[N_COINS] = self._unpack(packed_precisions) fee_receiver: address = factory.fee_receiver() balances: uint256[N_COINS] = self.balances @@ -1214,14 +1211,13 @@ def _claim_admin_fees(): def xp( balances: uint256[N_COINS], price_scale_packed: uint256, - precisions: uint256[N_COINS] ) -> uint256[N_COINS]: result: uint256[N_COINS] = balances - result[0] *= precisions[0] + 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 @@ -1327,13 +1323,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(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): @@ -1801,14 +1796,7 @@ def fee() -> uint256: removed. @return uint256 fee bps. """ - precisions: uint256[N_COINS] = self._unpack(packed_precisions) - return self._fee( - self.xp( - self.balances, - self.price_scale_packed, - precisions - ) - ) + return self._fee(self.xp(self.balances, self.price_scale_packed)) @view @@ -1932,7 +1920,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(packed_precisions) + return PRECISIONS @external diff --git a/tests/boa/unitary/math/test_get_p.py b/tests/boa/unitary/math/test_get_p.py index b587241d..04c676a0 100644 --- a/tests/boa/unitary/math/test_get_p.py +++ b/tests/boa/unitary/math/test_get_p.py @@ -55,7 +55,6 @@ def _get_dydx_vyper(swap, i, j, price_calc): xp = swap.internal.xp( swap._storage.balances.get(), swap._storage.price_scale_packed.get(), - swap.precisions(), ) for k in range(3): diff --git a/tests/boa/unitary/math/test_get_p_expt.py b/tests/boa/unitary/math/test_get_p_expt.py index 2cedf3c9..f935b189 100644 --- a/tests/boa/unitary/math/test_get_p_expt.py +++ b/tests/boa/unitary/math/test_get_p_expt.py @@ -159,9 +159,7 @@ def _get_prices_vyper(swap, price_calc): for i in range(3): balances.append(swap.balances(i)) - xp = swap.internal.xp( - balances, swap._storage.price_scale_packed.get(), swap.precisions() - ) + xp = swap.internal.xp(balances, swap._storage.price_scale_packed.get()) D = swap.D() diff --git a/tests/boa/unitary/pool/test_oracles.py b/tests/boa/unitary/pool/test_oracles.py index 9b3f7e92..6221cc48 100644 --- a/tests/boa/unitary/pool/test_oracles.py +++ b/tests/boa/unitary/pool/test_oracles.py @@ -32,7 +32,6 @@ def get_D_oracle_input(swap, math_contract): xp = swap.internal.xp( swap._storage.balances.get(), swap._storage.price_scale_packed.get(), - swap.internal._unpack(swap._immutables.packed_precisions), ) D = math_contract.newton_D(A, gamma, xp) xcp = math_contract.geometric_mean(xp) From 991db54a68a74854bc28abee283bb0eb7970fa9e Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 4 Sep 2023 11:35:54 +0200 Subject: [PATCH 38/53] fix: use cached last_xcp insteadgit add . --- contracts/main/CurveTricryptoOptimizedWETH.vy | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index fb1c8272..ee4a3c8c 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -935,13 +935,11 @@ def tweak_price( self.price_oracle_packed = self._pack_prices(price_oracle) - # ------------------------------------------------- Update TVL oracle. + # ------------------------------------------------- Update xcp oracle. cached_xcp_oracle: uint256 = self.cached_xcp_oracle - last_cached_tvl: uint256 = self.last_xcp - self.cached_xcp_oracle = unsafe_div( - last_cached_tvl * (10**18 - alpha) + cached_xcp_oracle * alpha, + self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, 10**18 ) @@ -1749,8 +1747,7 @@ def xcp_oracle() -> uint256: ) ) - last_xcp: uint256 = self.get_xcp(self.D, self.price_scale_packed) - return (last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha) / 10**18 + return (self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha) / 10**18 return cached_xcp_oracle From 73a33980f4e49374fa3647750d0e579db0b755f9 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 4 Sep 2023 11:37:08 +0200 Subject: [PATCH 39/53] remove raising fees --- contracts/main/CurveTricryptoOptimizedWETH.vy | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index ee4a3c8c..7110352f 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1255,11 +1255,6 @@ 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]) - # During parameter ramping, we raise fees and disable admin fee claiming - if self.future_A_gamma_time > block.timestamp: # parameter ramping - fee_params[0] = 10**8 # set mid_fee to 100 basis points - fee_params[1] = 10**8 # set out_fee to 100 basis points - return unsafe_div( fee_params[0] * f + fee_params[1] * (10**18 - f), 10**18 From b67ad8061f1eef490c74e8883b33c9a4c9af5f51 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 4 Sep 2023 11:50:05 +0200 Subject: [PATCH 40/53] reduce number of reads of coin balance --- contracts/main/CurveTricryptoOptimizedWETH.vy | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 7110352f..96c8ccd5 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -317,14 +317,16 @@ def _transfer_in( """ received_amounts: uint256 = 0 coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - recorded_balance: uint256 = self.balances[_coin_idx] - - # Adjust balances before handling transfers - self.balances[_coin_idx] += dx # Handle transfers: if expect_optimistic_transfer: + # Get cached balance since last liquidity operation + recorded_balance: uint256 = self.balances[_coin_idx] + + # Adjust balances before handling transfers + self.balances[_coin_idx] += dx + # Only enabled in exchange_received: it expects the caller # of exchange_received to have sent tokens to the pool before # calling this method. @@ -332,6 +334,9 @@ def _transfer_in( else: + # Adjust balances before handling transfers + self.balances[_coin_idx] += dx + # EXTERNAL CALL assert ERC20(coins[_coin_idx]).transferFrom( sender, From 7ea427f9f187911517e586a16336d40b7c66415c Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Mon, 4 Sep 2023 14:24:01 +0200 Subject: [PATCH 41/53] remove invariant for fee check during ramp (in test) --- tests/boa/unitary/pool/stateful/test_ramp.py | 7 ------- tests/boa/unitary/pool/stateful/test_ramp_nocheck.py | 7 ------- 2 files changed, 14 deletions(-) diff --git a/tests/boa/unitary/pool/stateful/test_ramp.py b/tests/boa/unitary/pool/stateful/test_ramp.py index 1340b9d5..03ef8120 100644 --- a/tests/boa/unitary/pool/stateful/test_ramp.py +++ b/tests/boa/unitary/pool/stateful/test_ramp.py @@ -101,13 +101,6 @@ def virtual_price(self): # Invariant is not conserved here pass - @invariant() - def check_bumped_fee_during_ramp(self): - if self.swap.future_A_gamma_time() > boa.env.vm.state.timestamp: - assert ( - self.swap.fee() >= 10**8 - ) # Charge at least 100 basis points! - @invariant() def check_xcp_profit_a_doesnt_increase(self): assert self.swap.xcp_profit_a() == self.xcp_profit_a_init diff --git a/tests/boa/unitary/pool/stateful/test_ramp_nocheck.py b/tests/boa/unitary/pool/stateful/test_ramp_nocheck.py index 14c5a657..4ac789fc 100644 --- a/tests/boa/unitary/pool/stateful/test_ramp_nocheck.py +++ b/tests/boa/unitary/pool/stateful/test_ramp_nocheck.py @@ -77,13 +77,6 @@ def up_only_profit(self): # so we need to override super().up_only_profit() pass - @invariant() - def check_bumped_fee_during_ramp(self): - if self.swap.future_A_gamma_time() > boa.env.vm.state.timestamp: - assert ( - self.swap.fee() >= 10**8 - ) # Charge at least 100 basis points! - @invariant() def check_xcp_profit_a_doesnt_increase(self): assert self.swap.xcp_profit_a() == self.xcp_profit_a_init From ab7a8f0df60e269b92534268b6ac1b054b3bcaf8 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 5 Sep 2023 13:01:09 +0200 Subject: [PATCH 42/53] clean up _transfer_in --- contracts/main/CurveTricryptoOptimizedWETH.vy | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 96c8ccd5..9a746cc2 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -315,23 +315,23 @@ def _transfer_in( @params expect_optimistic_transfer bool True if pool expects user to transfer. This is only enabled for exchange_received. """ - received_amounts: uint256 = 0 coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - # Handle transfers: - if expect_optimistic_transfer: + 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. - # Get cached balance since last liquidity operation - recorded_balance: uint256 = self.balances[_coin_idx] + # 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. + assert coin_balance - self.balances[_coin_idx] >= dx # dev: user didn't give us coins - # Adjust balances before handling transfers + # Adjust balances self.balances[_coin_idx] += dx - # Only enabled in exchange_received: it expects the caller - # of exchange_received to have sent tokens to the pool before - # calling this method. - received_amounts = coin_balance - recorded_balance - else: # Adjust balances before handling transfers @@ -344,15 +344,8 @@ def _transfer_in( dx, default_return_value=True ) - received_amounts = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance - - # 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. - assert received_amounts >= dx # dev: user didn't give us coins + + assert ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance >= dx # dev: user didn't give us coins @internal From b7cc65fd2d8f8c434c8a509fb7581e93b7c49e21 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Fri, 8 Sep 2023 11:53:23 +0200 Subject: [PATCH 43/53] remove increaseAllowance and decreaseAllowance since we don't use it and it seems to be a phishing attack vector --- contracts/main/CurveTricryptoOptimizedWETH.vy | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 9a746cc2..31fc167f 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -1444,9 +1444,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. @@ -1456,51 +1453,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, From 8bbd3d80470e42a3f13e8b545ab811b3d7603453 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:59:27 +0200 Subject: [PATCH 44/53] break cei but use dx received for calcs instead --- contracts/main/CurveTricryptoOptimizedWETH.vy | 90 +++++++++---------- tests/boa/unitary/math/test_get_p_expt.py | 5 +- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 31fc167f..ed9003c5 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -302,10 +302,10 @@ def __init__( @internal def _transfer_in( _coin_idx: uint256, - dx: uint256, + _dx: uint256, sender: address, expect_optimistic_transfer: bool, -): +) -> uint256: """ @notice Transfers `_coin` from `sender` to `self` and calls `callback_sig` if it is not empty. @@ -314,6 +314,7 @@ def _transfer_in( @params sender address to transfer `_coin` from. @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) @@ -327,25 +328,27 @@ def _transfer_in( # 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. - assert coin_balance - self.balances[_coin_idx] >= dx # dev: user didn't give us coins + 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 - else: + return dx - # Adjust balances before handling transfers - self.balances[_coin_idx] += dx + # ----------------------------------------------- ERC20 transferFrom flow. - # EXTERNAL CALL - assert ERC20(coins[_coin_idx]).transferFrom( - sender, - self, - dx, - default_return_value=True - ) + # EXTERNAL CALL + assert ERC20(coins[_coin_idx]).transferFrom( + sender, + self, + _dx, + default_return_value=True + ) - assert ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance >= dx # dev: user didn't give us coins + dx: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance + self.balances[_coin_idx] += dx + return dx @internal @@ -467,9 +470,19 @@ def add_liquidity( # -------------------------------------- Update balances and calculate xp. xp_old: uint256[N_COINS] = xp + amounts_received: uint256[N_COINS] = empty(uint256[N_COINS]) + + ########################## TRANSFER IN <------- + for i in range(N_COINS): - bal: uint256 = xp[i] + amounts[i] - xp[i] = bal + if amounts[i] > 0: + amounts_received[i] = self._transfer_in( + i, + amounts[i], + msg.sender, + False, # <--------------------- Disable optimistic transfers. + ) + xp[i] = xp[i] + amounts_received[i] xx = xp @@ -537,23 +550,10 @@ def add_liquidity( assert d_token >= min_mint_amount, "Slippage" - # ---------------- transferFrom token into the pool ---------------------- - - for i in range(N_COINS): - - if amounts[i] > 0: - - # _transfer_out updates self.balances here. Update to state occurs - # before external calls: - self._transfer_in( - i, - amounts[i], - msg.sender, - False, # <--------------------- Disable optimistic transfers. - ) + # ---------------------------------------------- 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() # <--------- Auto-claim admin fees occasionally. @@ -775,7 +775,18 @@ def _exchange( 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 + + ########################## 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( @@ -835,18 +846,7 @@ def _exchange( packed_price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1]) - # ---------------------- Do Transfers in and out ------------------------- - - ########################## TRANSFER IN <------- - - # _transfer_in updates self.balances here. Update to state occurs before - # external calls: - self._transfer_in( - i, - dx, - sender, - expect_optimistic_transfer # <---- If True, pool expects dx tokens to - ) # be transferred in. + # --------------------------- Do Transfers out --------------------------- ########################## -------> TRANSFER OUT @@ -854,7 +854,7 @@ def _exchange( # 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 diff --git a/tests/boa/unitary/math/test_get_p_expt.py b/tests/boa/unitary/math/test_get_p_expt.py index f935b189..e55587e8 100644 --- a/tests/boa/unitary/math/test_get_p_expt.py +++ b/tests/boa/unitary/math/test_get_p_expt.py @@ -229,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, From a2b126cabbe90225590751d46b7bae68d879ea68 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 13 Sep 2023 10:48:28 +0200 Subject: [PATCH 45/53] fix test since we now acknowledge what goes in --- tests/boa/unitary/pool/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/boa/unitary/pool/test_exchange.py b/tests/boa/unitary/pool/test_exchange.py index 3452bfb9..c2488c19 100644 --- a/tests/boa/unitary/pool/test_exchange.py +++ b/tests/boa/unitary/pool/test_exchange.py @@ -157,7 +157,7 @@ def test_exchange_received_send_extra( assert amount == measured_i - 1 # <--- we sent 1 wei extra assert calculated == measured_j - assert d_balance_i == amount # <--- we sent 1 wei extra + assert d_balance_i == amount + 1 # <--- we sent 1 wei extra assert -d_balance_j == measured_j From 4630a275b8cd2292a1e8a86ed54274055a0196e0 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:11:49 +0200 Subject: [PATCH 46/53] add xcp time for oracle; make commit and apply for parameters a single step --- contracts/main/CurveTricryptoOptimizedWETH.vy | 119 ++++++------------ 1 file changed, 35 insertions(+), 84 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index ed9003c5..a28e895c 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -95,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 @@ -111,6 +102,7 @@ event NewParameters: allowed_extra_profit: uint256 adjustment_step: uint256 ma_time: uint256 + xcp_ma_time: uint256 event RampAgamma: initial_A: uint256 @@ -150,6 +142,7 @@ 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) @@ -189,10 +182,8 @@ 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 @@ -273,6 +264,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. @@ -880,28 +872,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 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 @@ -912,7 +897,7 @@ 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, @@ -936,14 +921,23 @@ def tweak_price( # ------------------------------------------------- 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 ) - # -------------------------------------------------------------------- - - self.last_prices_timestamp = block.timestamp # <---- Store timestamp. + # 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 @@ -1671,7 +1665,7 @@ 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.ma_time`. + determined by `self.xcp_ma_time`. `TVL` is xcp, calculated as either: 1. virtual_price * total_supply, OR 2. self.get_xcp(...), OR @@ -1684,10 +1678,9 @@ def xcp_oracle() -> uint256: if last_prices_timestamp < block.timestamp: - ma_time: uint256 = self._unpack(self.packed_rebalancing_params)[2] alpha: uint256 = MATH.wad_exp( -convert( - (block.timestamp - last_prices_timestamp) * 10**18 / ma_time, + (block.timestamp - last_prices_timestamp) * 10**18 / self.xcp_ma_time, int256, ) ) @@ -1961,13 +1954,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. @@ -1978,12 +1973,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.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 # ----------------------------- Set fee params --------------------------- @@ -2007,9 +1999,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 ----------------- @@ -2034,56 +2024,17 @@ def commit_new_parameters( [new_allowed_extra_profit, new_adjustment_step, new_ma_time] ) + # Set xcp oracle moving average window time: + new_xcp_ma_time: uint256 = _new_xcp_ma_time + # ---------------------------------- 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.admin() # dev: only owner - self.admin_actions_deadline = 0 From de7d4cdd67273afd65efdbc1693d9b3bb70ed4c7 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:14:01 +0200 Subject: [PATCH 47/53] fix: set rebalancing params to the right var --- contracts/main/CurveTricryptoOptimizedWETH.vy | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index a28e895c..f8b740df 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -163,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. @@ -2020,7 +2015,7 @@ def apply_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] ) From b30f59f5deed986d61856093d47f096e10d2320c Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:17:04 +0200 Subject: [PATCH 48/53] remove test that is no longer necessary' --- tests/boa/unitary/pool/test_exchange.py | 50 ------------------------- 1 file changed, 50 deletions(-) diff --git a/tests/boa/unitary/pool/test_exchange.py b/tests/boa/unitary/pool/test_exchange.py index c2488c19..f7906aa5 100644 --- a/tests/boa/unitary/pool/test_exchange.py +++ b/tests/boa/unitary/pool/test_exchange.py @@ -111,56 +111,6 @@ def test_exchange_received_success( assert -d_balance_j == measured_j -@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_received_send_extra( - swap_with_deposit, - views_contract, - coins, - user, - amount, - i, - j, -): - - if i == j or i > 2 or j > 2: - - return - - amount = amount * 10**18 // INITIAL_PRICES[i] - mint_for_testing(coins[i], user, amount + 1) # <--- mint 1 wei extra - - calculated = views_contract.get_dy(i, j, amount, swap_with_deposit) - - measured_i = coins[i].balanceOf(user) - measured_j = coins[j].balanceOf(user) - d_balance_i = swap_with_deposit.balances(i) - d_balance_j = swap_with_deposit.balances(j) - - with boa.env.prank(user): - coins[i].transfer(swap_with_deposit, amount + 1) # <--- send extra - swap_with_deposit.exchange_received( - i, j, amount, int(0.999 * calculated), user - ) - - measured_i -= coins[i].balanceOf(user) - measured_j = coins[j].balanceOf(user) - measured_j - d_balance_i = swap_with_deposit.balances(i) - d_balance_i - d_balance_j = swap_with_deposit.balances(j) - d_balance_j - - assert amount == measured_i - 1 # <--- we sent 1 wei extra - assert calculated == measured_j - - assert d_balance_i == amount + 1 # <--- we sent 1 wei extra - assert -d_balance_j == measured_j - - @given( amount=strategy( "uint256", min_value=10**10, max_value=2 * 10**6 * 10**18 From e925470083b3d3807c87bbca0b2e02416ea24026 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:09:59 +0200 Subject: [PATCH 49/53] fix: add checks for applying xcp_ma_time --- contracts/main/CurveTricryptoOptimizedWETH.vy | 9 ++- tests/boa/fixtures/pool.py | 4 +- .../unitary/pool/admin/test_commit_params.py | 33 +++++++---- .../pool/admin/test_revert_commit_params.py | 59 ++++--------------- tests/boa/unitary/pool/test_oracles.py | 18 +----- 5 files changed, 43 insertions(+), 80 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index f8b740df..3c07a022 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -463,6 +463,7 @@ def add_liquidity( for i in range(N_COINS): if amounts[i] > 0: + # Updates self.balances here: amounts_received[i] = self._transfer_in( i, amounts[i], @@ -1704,6 +1705,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 @@ -2021,6 +2023,11 @@ def apply_new_parameters( # 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 --------------------------------- @@ -2031,5 +2038,5 @@ def apply_new_parameters( new_allowed_extra_profit, new_adjustment_step, new_ma_time, - new_xcp_ma_time, + _new_xcp_ma_time, ) 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/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/test_oracles.py b/tests/boa/unitary/pool/test_oracles.py index 6221cc48..2a7bbbcf 100644 --- a/tests/boa/unitary/pool/test_oracles.py +++ b/tests/boa/unitary/pool/test_oracles.py @@ -26,19 +26,6 @@ def norm(price_oracle, price_scale): return sqrt(norm) -def get_D_oracle_input(swap, math_contract): - - A, gamma = swap.internal._A_gamma() - xp = swap.internal.xp( - swap._storage.balances.get(), - swap._storage.price_scale_packed.get(), - ) - D = math_contract.newton_D(A, gamma, xp) - xcp = math_contract.geometric_mean(xp) - - return D, xcp - - def test_initial(swap_with_deposit): for i in range(2): assert swap_with_deposit.price_scale(i) == INITIAL_PRICES[i + 1] @@ -140,10 +127,7 @@ def test_xcp_ma( amount = amount * 10**18 // INITIAL_PRICES[i] mint_for_testing(coins[i], user, amount) - rebal_params = swap_with_deposit.internal._unpack( - swap_with_deposit._storage.packed_rebalancing_params.get() - ) - ma_time = rebal_params[2] + ma_time = swap_with_deposit.xcp_ma_time() # swap to populate with boa.env.prank(user): From 59b387f7c1a8003b1b0e640cabefafd2cb6dd289 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:11:10 +0200 Subject: [PATCH 50/53] add exchange received note --- contracts/main/CurveTricryptoOptimizedWETH.vy | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 3c07a022..d0ad4ce0 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -407,6 +407,7 @@ def exchange_received( 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 From 6ed527dfeaf7591be6697c2864778931306e78bc Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:17:13 +0200 Subject: [PATCH 51/53] remove unused local var --- contracts/main/CurveTricryptoOptimizedWETH.vy | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index d0ad4ce0..98bcfd7b 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -444,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 @@ -473,8 +472,6 @@ def add_liquidity( ) xp[i] = xp[i] + amounts_received[i] - xx = xp - xp[0] *= PRECISIONS[0] xp_old[0] *= PRECISIONS[0] for i in range(1, N_COINS): From de23aceb08073f961762512b1ce3e2ad9211beca Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:22:12 +0200 Subject: [PATCH 52/53] reduce one redundant for-loop --- contracts/main/CurveTricryptoOptimizedWETH.vy | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/main/CurveTricryptoOptimizedWETH.vy b/contracts/main/CurveTricryptoOptimizedWETH.vy index 98bcfd7b..55154838 100644 --- a/contracts/main/CurveTricryptoOptimizedWETH.vy +++ b/contracts/main/CurveTricryptoOptimizedWETH.vy @@ -474,16 +474,16 @@ def add_liquidity( 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 - ) - - # recalc amountsp: for i in range(N_COINS): - if amounts[i] > 0: + + 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 ----------------------- From 7ab6023fa390fadb55aa8dcc3526ffd0e65d6964 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:29:08 +0200 Subject: [PATCH 53/53] deploy new tricrypto implementations --- README.md | 4 ++++ scripts/deploy.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) 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/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