diff --git a/.gitignore b/.gitignore index 9232552..eba381d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__ build/ reports/ node_modules +.build/ ### Python ### # Byte-compiled / optimized / DLL files diff --git a/contracts/implementations/plain-2/Plain2Balances.vy b/contracts/implementations/plain-2/Plain2Balances.vy index 61ee2c9..a64d840 100644 --- a/contracts/implementations/plain-2/Plain2Balances.vy +++ b/contracts/implementations/plain-2/Plain2Balances.vy @@ -1,6 +1,6 @@ -# @version 0.3.1 +# @version 0.3.9 """ -@title StableSwap +@title CurveStableSwap2Balances @author Curve.Fi @license Copyright (c) Curve.Fi, 2020-2021 - all rights reserved @notice 2 coin pool implementation with no lending @@ -73,6 +73,7 @@ event StopRampA: t: uint256 +N_COINS_256: constant(uint256) = 2 N_COINS: constant(int128) = 2 PRECISION: constant(uint256) = 10 ** 18 @@ -105,6 +106,10 @@ future_A_time: public(uint256) rate_multipliers: uint256[N_COINS] +last_prices_packed: uint256 +ma_exp_time: public(uint256) +ma_last_time: public(uint256) + name: public(String[64]) symbol: public(String[32]) @@ -145,7 +150,7 @@ def initialize( for i in range(N_COINS): coin: address = _coins[i] - if coin == ZERO_ADDRESS: + if coin == empty(address): break self.coins[i] = coin self.rate_multipliers[i] = _rate_multipliers[i] @@ -160,12 +165,14 @@ def initialize( self.name = name self.symbol = concat(_symbol, "-f") + self.ma_exp_time = 865 # set it to default = 10 mins + self.DOMAIN_SEPARATOR = keccak256( _abi_encode(EIP712_TYPEHASH, keccak256(name), keccak256(VERSION), chain.id, self) ) # fire a transfer event so block explorers identify the contract as an ERC20 - log Transfer(ZERO_ADDRESS, self, 0) + log Transfer(empty(address), self, 0) ### ERC20 Functionality ### @@ -213,7 +220,7 @@ def transferFrom(_from : address, _to : address, _value : uint256) -> bool: self._transfer(_from, _to, _value) _allowance: uint256 = self.allowance[_from][msg.sender] - if _allowance != MAX_UINT256: + if _allowance != max_value(uint256): self.allowance[_from][msg.sender] = _allowance - _value return True @@ -262,7 +269,7 @@ def permit( @param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner @return True, if transaction completes successfully """ - assert _owner != ZERO_ADDRESS + assert _owner != empty(address) assert block.timestamp <= _deadline nonce: uint256 = self.nonces[_owner] @@ -290,6 +297,27 @@ def permit( ### StableSwap Functionality ### + +@pure +@internal +def pack_prices(p1: uint256, p2: uint256) -> uint256: + assert p1 < 2**128 + assert p2 < 2**128 + return p1 | (p2 << 128) + + +@view +@external +def last_price() -> uint256: + return self.last_prices_packed & (2**128 - 1) + + +@view +@external +def ema_price() -> uint256: + return (self.last_prices_packed >> 128) + + @view @internal def _balances() -> uint256[N_COINS]: @@ -385,11 +413,11 @@ def get_D(_xp: uint256[N_COINS], _amp: uint256) -> uint256: return 0 D: uint256 = S - Ann: uint256 = _amp * N_COINS + Ann: uint256 = _amp * N_COINS_256 for i in range(255): D_P: uint256 = D * D / _xp[0] * D / _xp[1] / (N_COINS)**2 Dprev: uint256 = D - D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) + D = (Ann * S / A_PRECISION + D_P * N_COINS_256) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS_256 + 1) * D_P) # Equality with the precision of 1 if D > Dprev: if D - Dprev <= 1: @@ -409,8 +437,144 @@ def get_D_mem(_rates: uint256[N_COINS], _balances: uint256[N_COINS], _amp: uint2 return self.get_D(xp, _amp) +@internal @view +def _get_p(xp: uint256[N_COINS], amp: uint256, D: uint256) -> uint256: + # dx_0 / dx_1 only, however can have any number of coins in pool + ANN: uint256 = amp * N_COINS_256 + Dr: uint256 = D / (N_COINS**N_COINS) + for i in range(N_COINS): + Dr = Dr * D / xp[i] + return 10**18 * (ANN * xp[0] / A_PRECISION + Dr * xp[0] / xp[1]) / (ANN * xp[0] / A_PRECISION + Dr) + + @external +@view +def get_p() -> uint256: + amp: uint256 = self._A() + xp: uint256[N_COINS] = self._xp_mem(self.rate_multipliers, self._balances()) + D: uint256 = self.get_D(xp, amp) + return self._get_p(xp, amp, D) + + +@internal +@pure +def exp(x: int256) -> uint256: + + """ + @dev Calculates the natural exponential function of a signed integer with + a precision of 1e18. + @notice Note that this function consumes about 810 gas units. The implementation + is inspired by Remco Bloemen's implementation under the MIT license here: + https://xn--2-umb.com/22/exp-ln. + @dev This implementation is derived from Snekmate, which is authored + by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license. + https://github.com/pcaversaccio/snekmate + @param x The 32-byte variable. + @return int256 The 32-byte calculation result. + """ + value: int256 = x + + # If the result is `< 0.5`, we return zero. This happens when we have the following: + # "x <= floor(log(0.5e18) * 1e18) ~ -42e18". + if (x <= -42139678854452767551): + return empty(uint256) + + # When the result is "> (2 ** 255 - 1) / 1e18" we cannot represent it as a signed integer. + # This happens when "x >= floor(log((2 ** 255 - 1) / 1e18) * 1e18) ~ 135". + assert x < 135305999368893231589, "wad_exp overflow" + + # `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 ** 96" for higher + # intermediate precision and a binary base. This base conversion is a multiplication with + # "1e18 / 2 ** 96 = 5 ** 18 / 2 ** 78". + value = unsafe_div(x << 78, 5 ** 18) + + # Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 ** 96" by factoring out powers of two + # so that "exp(x) = exp(x') * 2 ** k", where `k` is a signer integer. Solving this gives + # "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]". + k: int256 = unsafe_add(unsafe_div(value << 96, 54916777467707473351141471128), 2 ** 95) >> 96 + value = unsafe_sub(value, unsafe_mul(k, 54916777467707473351141471128)) + + # Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic, + # we will multiply by a scaling factor later. + y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1346386616545796478920950773328), value) >> 96, 57155421227552351082224309758442) + p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94201549194550492254356042504812), y) >> 96,\ + 28719021644029726153956944680412240), value), 4385272521454847904659076985693276 << 96) + + # We leave `p` in the "2 ** 192" base so that we do not have to scale it up + # again for the division. + q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2855989394907223263936484059900), value) >> 96, 50020603652535783019961831881945) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 533845033583426703283633433725380) + q = unsafe_add(unsafe_mul(q, value) >> 96, 3604857256930695427073651918091429) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 14423608567350463180887372962807573) + q = unsafe_add(unsafe_mul(q, value) >> 96, 26449188498355588339934803723976023) + + # The polynomial `q` has no zeros in the range because all its roots are complex. + # No scaling is required, as `p` is already "2 ** 96" too large. Also, + # `r` is in the range "(0.09, 0.25) * 2**96" after the division. + r: int256 = unsafe_div(p, q) + + # To finalise the calculation, we have to multiply `r` by: + # - the scale factor "s = ~6.031367120", + # - the factor "2 ** k" from the range reduction, and + # - the factor "1e18 / 2 ** 96" for the base conversion. + # We do this all at once, with an intermediate result in "2**213" base, + # so that the final right shift always gives a positive value. + + # Note that to circumvent Vyper's safecast feature for the potentially + # negative parameter value `r`, we first convert `r` to `bytes32` and + # subsequently to `uint256`. Remember that the EVM default behaviour is + # to use two's complement representation to handle signed integers. + return unsafe_mul(convert(convert(r, bytes32), uint256), 3822833074963236453042738258902158003155416615667) >> convert(unsafe_sub(195, k), uint256) + + + +@internal +@view +def _ma_price() -> uint256: + ma_last_time: uint256 = self.ma_last_time + + pp: uint256 = self.last_prices_packed + last_price: uint256 = pp & (2**128 - 1) + last_ema_price: uint256 = pp >> 128 + + if ma_last_time < block.timestamp: + alpha: uint256 = self.exp(- convert((block.timestamp - ma_last_time) * 10**18 / self.ma_exp_time, int256)) + return (last_price * (10**18 - alpha) + last_ema_price * alpha) / 10**18 + + else: + return last_ema_price + + +@external +@view +@nonreentrant('lock') +def price_oracle() -> uint256: + return self._ma_price() + + +@internal +def save_p_from_price(last_price: uint256): + """ + Saves current price and its EMA + """ + if last_price != 0: + self.last_prices_packed = self.pack_prices(last_price, self._ma_price()) + if self.ma_last_time < block.timestamp: + self.ma_last_time = block.timestamp + + +@internal +def save_p(xp: uint256[N_COINS], amp: uint256, D: uint256): + """ + Saves current price and its EMA + """ + self.save_p_from_price(self._get_p(xp, amp, D)) + + +@view +@external +@nonreentrant('lock') def get_virtual_price() -> uint256: """ @notice The current virtual price of the pool LP token @@ -482,20 +646,12 @@ def add_liquidity( for i in range(N_COINS): amount: uint256 = _amounts[i] if amount > 0: + coin: address = self.coins[i] initial: uint256 = ERC20(coin).balanceOf(self) - response: Bytes[32] = raw_call( - coin, - concat( - method_id("transferFrom(address,address,uint256)"), - convert(msg.sender, bytes32), - convert(self, bytes32), - convert(amount, bytes32), - ), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) # dev: failed transfer + + assert ERC20(coin).transferFrom(msg.sender, self, amount, default_return_value=True) + new_balances[i] += ERC20(coin).balanceOf(self) - initial else: assert total_supply != 0 # dev: initial deposit requires all coins @@ -510,7 +666,7 @@ def add_liquidity( mint_amount: uint256 = 0 if total_supply > 0: # Only account for fees if we are not the first to deposit - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + base_fee: uint256 = self.fee * N_COINS_256 / (4 * (N_COINS_256 - 1)) for i in range(N_COINS): ideal_balance: uint256 = D1 * old_balances[i] / D0 difference: uint256 = 0 @@ -522,8 +678,11 @@ def add_liquidity( fees[i] = base_fee * difference / FEE_DENOMINATOR self.admin_balances[i] += fees[i] * ADMIN_FEE / FEE_DENOMINATOR new_balances[i] -= fees[i] + + xp: uint256[N_COINS] = self._xp_mem(rates, new_balances) D2: uint256 = self.get_D_mem(rates, new_balances, amp) mint_amount = total_supply * (D2 - D0) / D0 + self.save_p(xp, amp, D2) else: mint_amount = D1 # Take the dust if there was any @@ -533,7 +692,7 @@ def add_liquidity( total_supply += mint_amount self.balanceOf[_receiver] += mint_amount self.totalSupply = total_supply - log Transfer(ZERO_ADDRESS, _receiver, mint_amount) + log Transfer(empty(address), _receiver, mint_amount) log AddLiquidity(msg.sender, _amounts, fees, D1, total_supply) @@ -542,7 +701,14 @@ def add_liquidity( @view @internal -def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS]) -> uint256: +def get_y( + i: int128, + j: int128, + x: uint256, + xp: uint256[N_COINS], + amp: uint256, + D: uint256, +) -> uint256: """ Calculate x[j] if one makes x[i] = x @@ -562,13 +728,11 @@ def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS]) -> uint256: assert i >= 0 assert i < N_COINS - amp: uint256 = self._A() - D: uint256 = self.get_D(xp, amp) S_: uint256 = 0 _x: uint256 = 0 y_prev: uint256 = 0 c: uint256 = D - Ann: uint256 = amp * N_COINS + Ann: uint256 = amp * N_COINS_256 for _i in range(N_COINS): if _i == i: @@ -578,9 +742,9 @@ def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS]) -> uint256: else: continue S_ += _x - c = c * D / (_x * N_COINS) + c = c * D / (_x * N_COINS_256) - c = c * D * A_PRECISION / (Ann * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS_256) b: uint256 = S_ + D * A_PRECISION / Ann # - D y: uint256 = D @@ -612,7 +776,11 @@ def get_dy(i: int128, j: int128, dx: uint256) -> uint256: xp: uint256[N_COINS] = self._xp_mem(rates, self._balances()) x: uint256 = xp[i] + (dx * rates[i] / PRECISION) - y: uint256 = self.get_y(i, j, x, xp) + + amp: uint256 = self._A() + D: uint256 = self.get_D(xp, amp) + y: uint256 = self.get_y(i, j, x, xp, amp, D) + dy: uint256 = xp[j] - y - 1 fee: uint256 = self.fee * dy / FEE_DENOMINATOR return (dy - fee) * PRECISION / rates[j] @@ -642,22 +810,16 @@ def exchange( coin: address = self.coins[i] dx: uint256 = ERC20(coin).balanceOf(self) - response: Bytes[32] = raw_call( - coin, - concat( - method_id("transferFrom(address,address,uint256)"), - convert(msg.sender, bytes32), - convert(self, bytes32), - convert(_dx, bytes32), - ), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + + assert ERC20(coin).transferFrom(msg.sender, self, _dx, default_return_value=True) + dx = ERC20(coin).balanceOf(self) - dx x: uint256 = xp[i] + dx * rates[i] / PRECISION - y: uint256 = self.get_y(i, j, x, xp) + + amp: uint256 = self._A() + D: uint256 = self.get_D(xp, amp) + y: uint256 = self.get_y(i, j, x, xp, amp, D) dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR @@ -666,19 +828,15 @@ def exchange( dy = (dy - dy_fee) * PRECISION / rates[j] assert dy >= _min_dy, "Exchange resulted in fewer coins than expected" + # xp is not used anymore, so we reuse it for price calc + xp[i] = x + xp[j] = y + # D is not changed because we did not apply a fee + self.save_p(xp, amp, D) + self.admin_balances[j] += (dy_fee * ADMIN_FEE / FEE_DENOMINATOR) * PRECISION / rates[j] - response = raw_call( - self.coins[j], - concat( - method_id("transfer(address,uint256)"), - convert(_receiver, bytes32), - convert(dy, bytes32), - ), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + assert ERC20(self.coins[j]).transfer(_receiver, dy, default_return_value=True) log TokenExchange(msg.sender, i, _dx, j, dy) @@ -709,22 +867,12 @@ def remove_liquidity( assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected" amounts[i] = value - response: Bytes[32] = raw_call( - self.coins[i], - concat( - method_id("transfer(address,uint256)"), - convert(_receiver, bytes32), - convert(value, bytes32), - ), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + assert ERC20(self.coins[i]).transfer(_receiver, value, default_return_value=True) total_supply -= _burn_amount self.balanceOf[msg.sender] -= _burn_amount self.totalSupply = total_supply - log Transfer(msg.sender, ZERO_ADDRESS, _burn_amount) + log Transfer(msg.sender, empty(address), _burn_amount) log RemoveLiquidity(msg.sender, amounts, empty(uint256[N_COINS]), total_supply) @@ -755,21 +903,12 @@ def remove_liquidity_imbalance( amount: uint256 = _amounts[i] if amount != 0: new_balances[i] -= amount - response: Bytes[32] = raw_call( - self.coins[i], - concat( - method_id("transfer(address,uint256)"), - convert(_receiver, bytes32), - convert(amount, bytes32), - ), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + assert ERC20(self.coins[i]).transfer(_receiver, amount, default_return_value=True) + D1: uint256 = self.get_D_mem(rates, new_balances, amp) fees: uint256[N_COINS] = empty(uint256[N_COINS]) - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + base_fee: uint256 = self.fee * N_COINS_256 / (4 * (N_COINS_256 - 1)) for i in range(N_COINS): ideal_balance: uint256 = D1 * old_balances[i] / D0 difference: uint256 = 0 @@ -783,6 +922,8 @@ def remove_liquidity_imbalance( new_balances[i] -= fees[i] D2: uint256 = self.get_D_mem(rates, new_balances, amp) + self.save_p(new_balances, amp, D2) + total_supply: uint256 = self.totalSupply burn_amount: uint256 = ((D0 - D2) * total_supply / D0) + 1 assert burn_amount > 1 # dev: zero tokens burned @@ -791,7 +932,7 @@ def remove_liquidity_imbalance( total_supply -= burn_amount self.totalSupply = total_supply self.balanceOf[msg.sender] -= burn_amount - log Transfer(msg.sender, ZERO_ADDRESS, burn_amount) + log Transfer(msg.sender, empty(address), burn_amount) log RemoveLiquidityImbalance(msg.sender, _amounts, fees, D1, total_supply) return burn_amount @@ -818,7 +959,7 @@ def get_y_D(A: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: _x: uint256 = 0 y_prev: uint256 = 0 c: uint256 = D - Ann: uint256 = A * N_COINS + Ann: uint256 = A * N_COINS_256 for _i in range(N_COINS): if _i != i: @@ -826,9 +967,9 @@ def get_y_D(A: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: else: continue S_ += _x - c = c * D / (_x * N_COINS) + c = c * D / (_x * N_COINS_256) - c = c * D * A_PRECISION / (Ann * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS_256) b: uint256 = S_ + D * A_PRECISION / Ann y: uint256 = D @@ -847,7 +988,7 @@ def get_y_D(A: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: @view @internal -def _calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256[2]: +def _calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256[3]: # First, need to calculate # * Get current D # * Solve Eqn against y_i for D - _token_amount @@ -860,7 +1001,7 @@ def _calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256[2]: D1: uint256 = D0 - _burn_amount * D0 / total_supply new_y: uint256 = self.get_y_D(amp, i, xp, D1) - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + base_fee: uint256 = self.fee * N_COINS_256 / (4 * (N_COINS_256 - 1)) xp_reduced: uint256[N_COINS] = empty(uint256[N_COINS]) for j in range(N_COINS): @@ -876,7 +1017,12 @@ def _calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256[2]: dy_0: uint256 = (xp[i] - new_y) * PRECISION / rates[i] # w/o fees dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors - return [dy, dy_0 - dy] + xp[i] = new_y + last_p: uint256 = 0 + if new_y > 0: + last_p = self._get_p(xp, amp, D1) + + return [dy, dy_0 - dy, last_p] @view @@ -907,29 +1053,21 @@ def remove_liquidity_one_coin( @param _receiver Address that receives the withdrawn coins @return Amount of coin received """ - dy: uint256[2] = self._calc_withdraw_one_coin(_burn_amount, i) + dy: uint256[3] = self._calc_withdraw_one_coin(_burn_amount, i) assert dy[0] >= _min_received, "Not enough coins removed" self.admin_balances[i] += dy[1] * ADMIN_FEE / FEE_DENOMINATOR total_supply: uint256 = self.totalSupply - _burn_amount self.totalSupply = total_supply self.balanceOf[msg.sender] -= _burn_amount - log Transfer(msg.sender, ZERO_ADDRESS, _burn_amount) + log Transfer(msg.sender, empty(address), _burn_amount) - response: Bytes[32] = raw_call( - self.coins[i], - concat( - method_id("transfer(address,uint256)"), - convert(_receiver, bytes32), - convert(dy[0], bytes32), - ), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + assert ERC20(self.coins[i]).transfer(_receiver, dy[0], default_return_value=True) log RemoveLiquidityOne(msg.sender, _burn_amount, dy[0], total_supply) + self.save_p_from_price(dy[2]) + return dy[0] @@ -978,17 +1116,18 @@ def withdraw_admin_fees(): amount: uint256 = self.admin_balances[i] if amount != 0: coin: address = self.coins[i] - raw_call( - coin, - concat( - method_id("transfer(address,uint256)"), - convert(receiver, bytes32), - convert(amount, bytes32) - ) - ) + assert ERC20(coin).transfer(receiver, amount, default_return_value=True) self.admin_balances[i] = 0 +@external +def set_ma_exp_time(_ma_exp_time: uint256): + assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert _ma_exp_time != 0 + + self.ma_exp_time = _ma_exp_time + + @pure @external def version() -> String[8]: diff --git a/contracts/implementations/plain-2/Plain2Price.vy b/contracts/implementations/plain-2/Plain2Price.vy index 7bbc02c..b291a37 100644 --- a/contracts/implementations/plain-2/Plain2Price.vy +++ b/contracts/implementations/plain-2/Plain2Price.vy @@ -1,9 +1,9 @@ -# @version 0.3.1 +# @version 0.3.9 """ -@title StableSwap +@title CurveStableSwap2Prices @author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2021 - all rights reserved -@notice 2 coin pool implementation with no lending +@license Copyright (c) Curve.Fi, 2023 - all rights reserved +@notice 2 coin pool implementation for tokens with rate oracles. @dev ERC20 support for return True/revert, return True/False, return None """ @@ -73,6 +73,7 @@ event StopRampA: N_COINS: constant(int128) = 2 +N_COINS_256: constant(uint256) = 2 PRECISION: constant(uint256) = 10 ** 18 FEE_DENOMINATOR: constant(uint256) = 10 ** 10 @@ -90,7 +91,7 @@ PERMIT_TYPEHASH: constant(bytes32) = keccak256("Permit(address owner,address spe ERC1271_MAGIC_VAL: constant(bytes32) = 0x1626ba7e00000000000000000000000000000000000000000000000000000000 VERSION: constant(String[8]) = "v5.0.0" -BIT_MASK: constant(uint256) = shift(2**32 - 1, 224) +BIT_MASK: constant(uint256) = (2**32 - 1 << 224) factory: address @@ -108,6 +109,9 @@ future_A_time: public(uint256) rate_multipliers: uint256[N_COINS] # [bytes4 method_id][bytes8 ][bytes20 oracle] oracles: uint256[N_COINS] +last_prices_packed: uint256 +ma_exp_time: public(uint256) +ma_last_time: public(uint256) name: public(String[64]) symbol: public(String[32]) @@ -152,7 +156,7 @@ def initialize( for i in range(N_COINS): coin: address = _coins[i] - if coin == ZERO_ADDRESS: + if coin == empty(address): break self.coins[i] = coin self.rate_multipliers[i] = _rate_multipliers[i] @@ -167,12 +171,14 @@ def initialize( self.name = name self.symbol = concat(_symbol, "-f") + self.ma_exp_time = 865 # set it to default = 10 mins + self.DOMAIN_SEPARATOR = keccak256( _abi_encode(EIP712_TYPEHASH, keccak256(name), keccak256(VERSION), chain.id, self) ) # fire a transfer event so block explorers identify the contract as an ERC20 - log Transfer(ZERO_ADDRESS, self, 0) + log Transfer(empty(address), self, 0) ### ERC20 Functionality ### @@ -220,7 +226,7 @@ def transferFrom(_from : address, _to : address, _value : uint256) -> bool: self._transfer(_from, _to, _value) _allowance: uint256 = self.allowance[_from][msg.sender] - if _allowance != MAX_UINT256: + if _allowance != max_value(uint256): self.allowance[_from][msg.sender] = _allowance - _value return True @@ -269,7 +275,7 @@ def permit( @param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner @return True, if transaction completes successfully """ - assert _owner != ZERO_ADDRESS + assert _owner != empty(address) assert block.timestamp <= _deadline nonce: uint256 = self.nonces[_owner] @@ -297,10 +303,31 @@ def permit( ### StableSwap Functionality ### + +@pure +@internal +def pack_prices(p1: uint256, p2: uint256) -> uint256: + assert p1 < 2**128 + assert p2 < 2**128 + return p1 | (p2 << 128) + + +@view +@external +def last_price() -> uint256: + return self.last_prices_packed & (2**128 - 1) + + +@view +@external +def ema_price() -> uint256: + return (self.last_prices_packed >> 128) + + @view @internal def _stored_rates() -> uint256[N_COINS]: - assert self.originator == ZERO_ADDRESS + assert self.originator == empty(address) rates: uint256[N_COINS] = self.rate_multipliers for i in range(N_COINS): @@ -311,7 +338,7 @@ def _stored_rates() -> uint256[N_COINS]: # NOTE: assumed that response is of precision 10**18 response: Bytes[32] = raw_call( convert(oracle % 2**160, address), - _abi_encode(bitwise_and(oracle, BIT_MASK)), + _abi_encode(oracle & BIT_MASK), max_outsize=32, is_static_call=True, ) @@ -394,11 +421,11 @@ def get_D(_xp: uint256[N_COINS], _amp: uint256) -> uint256: return 0 D: uint256 = S - Ann: uint256 = _amp * N_COINS + Ann: uint256 = _amp * N_COINS_256 for i in range(255): D_P: uint256 = D * D / _xp[0] * D / _xp[1] / (N_COINS)**2 Dprev: uint256 = D - D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) + D = (Ann * S / A_PRECISION + D_P * N_COINS_256) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS_256 + 1) * D_P) # Equality with the precision of 1 if D > Dprev: if D - Dprev <= 1: @@ -418,8 +445,144 @@ def get_D_mem(_rates: uint256[N_COINS], _balances: uint256[N_COINS], _amp: uint2 return self.get_D(xp, _amp) +@internal +@view +def _get_p(xp: uint256[N_COINS], amp: uint256, D: uint256) -> uint256: + # dx_0 / dx_1 only, however can have any number of coins in pool + ANN: uint256 = amp * N_COINS_256 + Dr: uint256 = D / (N_COINS**N_COINS) + for i in range(N_COINS): + Dr = Dr * D / xp[i] + return 10**18 * (ANN * xp[0] / A_PRECISION + Dr * xp[0] / xp[1]) / (ANN * xp[0] / A_PRECISION + Dr) + + +@external +@view +def get_p() -> uint256: + amp: uint256 = self._A() + xp: uint256[N_COINS] = self._xp_mem(self._stored_rates(), self.balances) + D: uint256 = self.get_D(xp, amp) + return self._get_p(xp, amp, D) + + +@internal +@pure +def exp(x: int256) -> uint256: + + """ + @dev Calculates the natural exponential function of a signed integer with + a precision of 1e18. + @notice Note that this function consumes about 810 gas units. The implementation + is inspired by Remco Bloemen's implementation under the MIT license here: + https://xn--2-umb.com/22/exp-ln. + @dev This implementation is derived from Snekmate, which is authored + by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license. + https://github.com/pcaversaccio/snekmate + @param x The 32-byte variable. + @return int256 The 32-byte calculation result. + """ + value: int256 = x + + # If the result is `< 0.5`, we return zero. This happens when we have the following: + # "x <= floor(log(0.5e18) * 1e18) ~ -42e18". + if (x <= -42139678854452767551): + return empty(uint256) + + # When the result is "> (2 ** 255 - 1) / 1e18" we cannot represent it as a signed integer. + # This happens when "x >= floor(log((2 ** 255 - 1) / 1e18) * 1e18) ~ 135". + assert x < 135305999368893231589, "wad_exp overflow" + + # `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 ** 96" for higher + # intermediate precision and a binary base. This base conversion is a multiplication with + # "1e18 / 2 ** 96 = 5 ** 18 / 2 ** 78". + value = unsafe_div(x << 78, 5 ** 18) + + # Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 ** 96" by factoring out powers of two + # so that "exp(x) = exp(x') * 2 ** k", where `k` is a signer integer. Solving this gives + # "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]". + k: int256 = unsafe_add(unsafe_div(value << 96, 54916777467707473351141471128), 2 ** 95) >> 96 + value = unsafe_sub(value, unsafe_mul(k, 54916777467707473351141471128)) + + # Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic, + # we will multiply by a scaling factor later. + y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1346386616545796478920950773328), value) >> 96, 57155421227552351082224309758442) + p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94201549194550492254356042504812), y) >> 96,\ + 28719021644029726153956944680412240), value), 4385272521454847904659076985693276 << 96) + + # We leave `p` in the "2 ** 192" base so that we do not have to scale it up + # again for the division. + q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2855989394907223263936484059900), value) >> 96, 50020603652535783019961831881945) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 533845033583426703283633433725380) + q = unsafe_add(unsafe_mul(q, value) >> 96, 3604857256930695427073651918091429) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 14423608567350463180887372962807573) + q = unsafe_add(unsafe_mul(q, value) >> 96, 26449188498355588339934803723976023) + + # The polynomial `q` has no zeros in the range because all its roots are complex. + # No scaling is required, as `p` is already "2 ** 96" too large. Also, + # `r` is in the range "(0.09, 0.25) * 2**96" after the division. + r: int256 = unsafe_div(p, q) + + # To finalise the calculation, we have to multiply `r` by: + # - the scale factor "s = ~6.031367120", + # - the factor "2 ** k" from the range reduction, and + # - the factor "1e18 / 2 ** 96" for the base conversion. + # We do this all at once, with an intermediate result in "2**213" base, + # so that the final right shift always gives a positive value. + + # Note that to circumvent Vyper's safecast feature for the potentially + # negative parameter value `r`, we first convert `r` to `bytes32` and + # subsequently to `uint256`. Remember that the EVM default behaviour is + # to use two's complement representation to handle signed integers. + return unsafe_mul(convert(convert(r, bytes32), uint256), 3822833074963236453042738258902158003155416615667) >> convert(unsafe_sub(195, k), uint256) + + + +@internal +@view +def _ma_price() -> uint256: + ma_last_time: uint256 = self.ma_last_time + + pp: uint256 = self.last_prices_packed + last_price: uint256 = pp & (2**128 - 1) + last_ema_price: uint256 = pp >> 128 + + if ma_last_time < block.timestamp: + alpha: uint256 = self.exp(- convert((block.timestamp - ma_last_time) * 10**18 / self.ma_exp_time, int256)) + return (last_price * (10**18 - alpha) + last_ema_price * alpha) / 10**18 + + else: + return last_ema_price + + +@external +@view +@nonreentrant('lock') +def price_oracle() -> uint256: + return self._ma_price() + + +@internal +def save_p_from_price(last_price: uint256): + """ + Saves current price and its EMA + """ + if last_price != 0: + self.last_prices_packed = self.pack_prices(last_price, self._ma_price()) + if self.ma_last_time < block.timestamp: + self.ma_last_time = block.timestamp + + +@internal +def save_p(xp: uint256[N_COINS], amp: uint256, D: uint256): + """ + Saves current price and its EMA + """ + self.save_p_from_price(self._get_p(xp, amp, D)) + + @view @external +@nonreentrant('lock') def get_virtual_price() -> uint256: """ @notice The current virtual price of the pool LP token @@ -491,18 +654,7 @@ def add_liquidity( for i in range(N_COINS): amount: uint256 = _amounts[i] if amount > 0: - response: Bytes[32] = raw_call( - self.coins[i], - _abi_encode( - msg.sender, - self, - amount, - method_id=method_id("transferFrom(address,address,uint256)") - ), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) # dev: failed transfer + assert ERC20(self.coins[i]).transferFrom(msg.sender, self, amount, default_return_value=True) new_balances[i] += amount # end "safeTransferFrom" else: @@ -518,7 +670,7 @@ def add_liquidity( mint_amount: uint256 = 0 if total_supply > 0: # Only account for fees if we are not the first to deposit - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + base_fee: uint256 = self.fee * N_COINS_256 / (4 * (N_COINS_256 - 1)) for i in range(N_COINS): ideal_balance: uint256 = D1 * old_balances[i] / D0 difference: uint256 = 0 @@ -530,8 +682,11 @@ def add_liquidity( fees[i] = base_fee * difference / FEE_DENOMINATOR self.balances[i] = new_balance - (fees[i] * ADMIN_FEE / FEE_DENOMINATOR) new_balances[i] -= fees[i] + + xp: uint256[N_COINS] = self._xp_mem(rates, new_balances) D2: uint256 = self.get_D_mem(rates, new_balances, amp) mint_amount = total_supply * (D2 - D0) / D0 + self.save_p(xp, amp, D2) else: self.balances = new_balances mint_amount = D1 # Take the dust if there was any @@ -542,7 +697,7 @@ def add_liquidity( total_supply += mint_amount self.balanceOf[_receiver] += mint_amount self.totalSupply = total_supply - log Transfer(ZERO_ADDRESS, _receiver, mint_amount) + log Transfer(empty(address), _receiver, mint_amount) log AddLiquidity(msg.sender, _amounts, fees, D1, total_supply) @@ -551,7 +706,14 @@ def add_liquidity( @view @internal -def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS]) -> uint256: +def get_y( + i: int128, + j: int128, + x: uint256, + xp: uint256[N_COINS], + amp: uint256, + D: uint256, +) -> uint256: """ Calculate x[j] if one makes x[i] = x @@ -571,13 +733,11 @@ def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS]) -> uint256: assert i >= 0 assert i < N_COINS - amp: uint256 = self._A() - D: uint256 = self.get_D(xp, amp) S_: uint256 = 0 _x: uint256 = 0 y_prev: uint256 = 0 c: uint256 = D - Ann: uint256 = amp * N_COINS + Ann: uint256 = amp * N_COINS_256 for _i in range(N_COINS): if _i == i: @@ -587,9 +747,9 @@ def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS]) -> uint256: else: continue S_ += _x - c = c * D / (_x * N_COINS) + c = c * D / (_x * N_COINS_256) - c = c * D * A_PRECISION / (Ann * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS_256) b: uint256 = S_ + D * A_PRECISION / Ann # - D y: uint256 = D @@ -621,7 +781,11 @@ def get_dy(i: int128, j: int128, dx: uint256) -> uint256: xp: uint256[N_COINS] = self._xp_mem(rates, self.balances) x: uint256 = xp[i] + (dx * rates[i] / PRECISION) - y: uint256 = self.get_y(i, j, x, xp) + + amp: uint256 = self._A() + D: uint256 = self.get_D(xp, amp) + y: uint256 = self.get_y(i, j, x, xp, amp, D) + dy: uint256 = xp[j] - y - 1 fee: uint256 = self.fee * dy / FEE_DENOMINATOR return (dy - fee) * PRECISION / rates[j] @@ -650,7 +814,10 @@ def exchange( xp: uint256[N_COINS] = self._xp_mem(rates, old_balances) x: uint256 = xp[i] + _dx * rates[i] / PRECISION - y: uint256 = self.get_y(i, j, x, xp) + + amp: uint256 = self._A() + D: uint256 = self.get_D(xp, amp) + y: uint256 = self.get_y(i, j, x, xp, amp, D) dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR @@ -667,26 +834,14 @@ def exchange( # When rounding errors happen, we undercharge admin fee in favor of LP self.balances[j] = old_balances[j] - dy - dy_admin_fee - response: Bytes[32] = raw_call( - self.coins[i], - _abi_encode( - msg.sender, - self, - _dx, - method_id=method_id("transferFrom(address,address,uint256)") - ), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + # xp is not used anymore, so we reuse it for price calc + xp[i] = x + xp[j] = y + # D is not changed because we did not apply a fee + self.save_p(xp, amp, D) - response = raw_call( - self.coins[j], - _abi_encode(_receiver, dy, method_id=method_id("transfer(address,uint256)")), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + assert ERC20(self.coins[i]).transferFrom(msg.sender, self, _dx, default_return_value=True) + assert ERC20(self.coins[j]).transfer(_receiver, dy, default_return_value=True) log TokenExchange(msg.sender, i, _dx, j, dy) @@ -718,18 +873,12 @@ def remove_liquidity( self.balances[i] = old_balance - value amounts[i] = value - response: Bytes[32] = raw_call( - self.coins[i], - _abi_encode(_receiver, value, method_id=method_id("transfer(address,uint256)")), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + assert ERC20(self.coins[i]).transfer(_receiver, value, default_return_value=True) total_supply -= _burn_amount self.balanceOf[msg.sender] -= _burn_amount self.totalSupply = total_supply - log Transfer(msg.sender, ZERO_ADDRESS, _burn_amount) + log Transfer(msg.sender, empty(address), _burn_amount) log RemoveLiquidity(msg.sender, amounts, empty(uint256[N_COINS]), total_supply) @@ -760,17 +909,11 @@ def remove_liquidity_imbalance( amount: uint256 = _amounts[i] if amount != 0: new_balances[i] -= amount - response: Bytes[32] = raw_call( - self.coins[i], - _abi_encode(_receiver, amount, method_id=method_id("transfer(address,uint256)")), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + assert ERC20(self.coins[i]).transfer(_receiver, amount, default_return_value=True) D1: uint256 = self.get_D_mem(rates, new_balances, amp) fees: uint256[N_COINS] = empty(uint256[N_COINS]) - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + base_fee: uint256 = self.fee * N_COINS_256 / (4 * (N_COINS_256 - 1)) for i in range(N_COINS): ideal_balance: uint256 = D1 * old_balances[i] / D0 difference: uint256 = 0 @@ -784,6 +927,8 @@ def remove_liquidity_imbalance( new_balances[i] -= fees[i] D2: uint256 = self.get_D_mem(rates, new_balances, amp) + self.save_p(new_balances, amp, D2) + total_supply: uint256 = self.totalSupply burn_amount: uint256 = ((D0 - D2) * total_supply / D0) + 1 assert burn_amount > 1 # dev: zero tokens burned @@ -792,7 +937,7 @@ def remove_liquidity_imbalance( total_supply -= burn_amount self.totalSupply = total_supply self.balanceOf[msg.sender] -= burn_amount - log Transfer(msg.sender, ZERO_ADDRESS, burn_amount) + log Transfer(msg.sender, empty(address), burn_amount) log RemoveLiquidityImbalance(msg.sender, _amounts, fees, D1, total_supply) return burn_amount @@ -819,7 +964,7 @@ def get_y_D(A: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: _x: uint256 = 0 y_prev: uint256 = 0 c: uint256 = D - Ann: uint256 = A * N_COINS + Ann: uint256 = A * N_COINS_256 for _i in range(N_COINS): if _i != i: @@ -827,9 +972,9 @@ def get_y_D(A: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: else: continue S_ += _x - c = c * D / (_x * N_COINS) + c = c * D / (_x * N_COINS_256) - c = c * D * A_PRECISION / (Ann * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS_256) b: uint256 = S_ + D * A_PRECISION / Ann y: uint256 = D @@ -848,7 +993,7 @@ def get_y_D(A: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: @view @internal -def _calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256[2]: +def _calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256[3]: # First, need to calculate # * Get current D # * Solve Eqn against y_i for D - _token_amount @@ -861,7 +1006,7 @@ def _calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256[2]: D1: uint256 = D0 - _burn_amount * D0 / total_supply new_y: uint256 = self.get_y_D(amp, i, xp, D1) - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + base_fee: uint256 = self.fee * N_COINS_256 / (4 * (N_COINS_256 - 1)) xp_reduced: uint256[N_COINS] = empty(uint256[N_COINS]) for j in range(N_COINS): @@ -877,7 +1022,12 @@ def _calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256[2]: dy_0: uint256 = (xp[i] - new_y) * PRECISION / rates[i] # w/o fees dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors - return [dy, dy_0 - dy] + xp[i] = new_y + last_p: uint256 = 0 + if new_y > 0: + last_p = self._get_p(xp, amp, D1) + + return [dy, dy_0 - dy, last_p] @view @@ -908,25 +1058,21 @@ def remove_liquidity_one_coin( @param _receiver Address that receives the withdrawn coins @return Amount of coin received """ - dy: uint256[2] = self._calc_withdraw_one_coin(_burn_amount, i) + dy: uint256[3] = self._calc_withdraw_one_coin(_burn_amount, i) assert dy[0] >= _min_received, "Not enough coins removed" self.balances[i] -= (dy[0] + dy[1] * ADMIN_FEE / FEE_DENOMINATOR) total_supply: uint256 = self.totalSupply - _burn_amount self.totalSupply = total_supply self.balanceOf[msg.sender] -= _burn_amount - log Transfer(msg.sender, ZERO_ADDRESS, _burn_amount) + log Transfer(msg.sender, empty(address), _burn_amount) - response: Bytes[32] = raw_call( - self.coins[i], - _abi_encode(_receiver, dy[0], method_id=method_id("transfer(address,uint256)")), - max_outsize=32, - ) - if len(response) > 0: - assert convert(response, bool) + assert ERC20(self.coins[i]).transfer(_receiver, dy[0], default_return_value=True) log RemoveLiquidityOne(msg.sender, _burn_amount, dy[0], total_supply) + self.save_p_from_price(dy[2]) + return dy[0] @@ -980,14 +1126,11 @@ def withdraw_admin_fees(): for i in range(N_COINS): coin: address = self.coins[i] fees: uint256 = ERC20(coin).balanceOf(self) - self.balances[i] - raw_call( - coin, - _abi_encode(receiver, fees, method_id=method_id("transfer(address,uint256)")), - ) + assert ERC20(self.coins[i]).transfer(receiver, fees, default_return_value=True) @external -def set_oracles(_method_ids: uint256[N_COINS], _oracles: address[N_COINS]): +def set_oracles(_method_ids: bytes4[N_COINS], _oracles: address[N_COINS]): """ @notice Set the oracles used for calculating rates @dev if any value is empty, rate will fallback to value provided on initialize, one time use. @@ -998,10 +1141,17 @@ def set_oracles(_method_ids: uint256[N_COINS], _oracles: address[N_COINS]): assert msg.sender == self.originator for i in range(N_COINS): - assert shift(_method_ids[i], 32) == 0 - self.oracles[i] = bitwise_and(_method_ids[i], convert(_oracles[i], uint256)) + self.oracles[i] = convert(_method_ids[i], uint256) * 2**224 | convert(_oracles[i], uint256) + + self.originator = empty(address) + + +@external +def set_ma_exp_time(_ma_exp_time: uint256): + assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert _ma_exp_time != 0 - self.originator = ZERO_ADDRESS + self.ma_exp_time = _ma_exp_time @pure @@ -1016,4 +1166,4 @@ def version() -> String[8]: @view @external def oracle(_idx: uint256) -> address: - return convert(self.oracles[_idx] % 2**160, address) \ No newline at end of file + return convert(self.oracles[_idx] % 2**160, address)