From 90d53bd6f0ca445cd8fb305d7cedd18c7325f768 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 25 Jun 2024 08:56:27 +0200 Subject: [PATCH] deploy zksync: --- contracts/zksync/CurveCryptoMathOptimized3.vy | 879 +++++++ .../zksync/CurveCryptoViews3Optimized.vy | 414 ++++ contracts/zksync/CurveL2TricryptoFactory.vy | 461 ++++ contracts/zksync/CurveTricryptoOptimized.vy | 2080 ++++++++++++++++ .../zksync/CurveTricryptoOptimizedWETH.vy | 2157 +++++++++++++++++ deployments.yaml | 194 +- scripts/deploy_infra.py | 74 +- 7 files changed, 6148 insertions(+), 111 deletions(-) create mode 100644 contracts/zksync/CurveCryptoMathOptimized3.vy create mode 100644 contracts/zksync/CurveCryptoViews3Optimized.vy create mode 100644 contracts/zksync/CurveL2TricryptoFactory.vy create mode 100644 contracts/zksync/CurveTricryptoOptimized.vy create mode 100644 contracts/zksync/CurveTricryptoOptimizedWETH.vy diff --git a/contracts/zksync/CurveCryptoMathOptimized3.vy b/contracts/zksync/CurveCryptoMathOptimized3.vy new file mode 100644 index 0000000..b2b6e77 --- /dev/null +++ b/contracts/zksync/CurveCryptoMathOptimized3.vy @@ -0,0 +1,879 @@ +# pragma version 0.3.10 +# pragma evm-version paris + +# (c) Curve.Fi, 2020-2023 +# AMM Math for 3-coin Curve Cryptoswap Pools +# +# Unless otherwise agreed on, only contracts owned by Curve DAO or +# Swiss Stake GmbH are allowed to call this contract. + +""" +@title CurveTricryptoMathOptimized +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Curve AMM Math for 3 unpegged assets (e.g. ETH, BTC, USD). +""" + +N_COINS: constant(uint256) = 3 +A_MULTIPLIER: constant(uint256) = 10000 + +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA: constant(uint256) = 5 * 10**16 + +MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 100 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 + +version: public(constant(String[8])) = "v2.0.0" + + +# ------------------------ AMM math functions -------------------------------- + + +@external +@view +def get_y( + _ANN: uint256, _gamma: uint256, x: uint256[N_COINS], _D: uint256, i: uint256 +) -> uint256[2]: + """ + @notice Calculate x[i] given other balances x[0..N_COINS-1] and invariant D. + @dev ANN = A * N**N. + @param _ANN AMM.A() value. + @param _gamma AMM.gamma() value. + @param x Balances multiplied by prices and precisions of all coins. + @param _D Invariant. + @param i Index of coin to calculate y. + """ + + # Safety checks + assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A + assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D + + frac: uint256 = 0 + for k in range(3): + if k != i: + frac = x[k] * 10**18 / _D + assert frac > 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]" + # if above conditions are met, x[k] > 0 + + j: uint256 = 0 + k: uint256 = 0 + if i == 0: + j = 1 + k = 2 + elif i == 1: + j = 0 + k = 2 + elif i == 2: + j = 0 + k = 1 + + ANN: int256 = convert(_ANN, int256) + gamma: int256 = convert(_gamma, int256) + D: int256 = convert(_D, int256) + x_j: int256 = convert(x[j], int256) + x_k: int256 = convert(x[k], int256) + gamma2: int256 = unsafe_mul(gamma, gamma) + + a: int256 = 10**36 / 27 + + # 10**36/9 + 2*10**18*gamma/27 - D**2/x_j*gamma**2*ANN/27**2/convert(A_MULTIPLIER, int256)/x_k + b: int256 = ( + unsafe_add( + 10**36 / 9, + unsafe_div(unsafe_mul(2 * 10**18, gamma), 27) + ) + - unsafe_div( + unsafe_div( + unsafe_div( + unsafe_mul( + unsafe_div(unsafe_mul(D, D), x_j), + gamma2 + ) * ANN, + 27**2 + ), + convert(A_MULTIPLIER, int256) + ), + x_k, + ) + ) # <------- The first two expressions can be unsafe, and unsafely added. + + # 10**36/9 + gamma*(gamma + 4*10**18)/27 + gamma**2*(x_j+x_k-D)/D*ANN/27/convert(A_MULTIPLIER, int256) + c: int256 = ( + unsafe_add( + 10**36 / 9, + unsafe_div(unsafe_mul(gamma, unsafe_add(gamma, 4 * 10**18)), 27) + ) + + unsafe_div( + unsafe_div( + unsafe_mul( + unsafe_div(gamma2 * unsafe_sub(unsafe_add(x_j, x_k), D), D), + ANN + ), + 27 + ), + convert(A_MULTIPLIER, int256), + ) + ) # <--------- Same as above with the first two expressions. In the third + # expression, x_j + x_k will not overflow since we know their range from + # previous assert statements. + + # (10**18 + gamma)**2/27 + d: int256 = unsafe_div(unsafe_add(10**18, gamma)**2, 27) + + # abs(3*a*c/b - b) + d0: int256 = abs(unsafe_mul(3, a) * c / b - b) # <------------ a is smol. + + divider: int256 = 0 + if d0 > 10**48: + divider = 10**30 + elif d0 > 10**44: + divider = 10**26 + elif d0 > 10**40: + divider = 10**22 + elif d0 > 10**36: + divider = 10**18 + elif d0 > 10**32: + divider = 10**14 + elif d0 > 10**28: + divider = 10**10 + elif d0 > 10**24: + divider = 10**6 + elif d0 > 10**20: + divider = 10**2 + else: + divider = 1 + + additional_prec: int256 = 0 + if abs(a) > abs(b): + additional_prec = abs(unsafe_div(a, b)) + a = unsafe_div(unsafe_mul(a, additional_prec), divider) + b = unsafe_div(b * additional_prec, divider) + c = unsafe_div(c * additional_prec, divider) + d = unsafe_div(d * additional_prec, divider) + else: + additional_prec = abs(unsafe_div(b, a)) + a = unsafe_div(a / additional_prec, divider) + b = unsafe_div(unsafe_div(b, additional_prec), divider) + c = unsafe_div(unsafe_div(c, additional_prec), divider) + d = unsafe_div(unsafe_div(d, additional_prec), divider) + + # 3*a*c/b - b + _3ac: int256 = unsafe_mul(3, a) * c + delta0: int256 = unsafe_div(_3ac, b) - b + + # 9*a*c/b - 2*b - 27*a**2/b*d/b + delta1: int256 = ( + unsafe_div(3 * _3ac, b) + - unsafe_mul(2, b) + - unsafe_div(unsafe_div(27 * a**2, b) * d, b) + ) + + # delta1**2 + 4*delta0**2/b*delta0 + sqrt_arg: int256 = ( + delta1**2 + + unsafe_div(4 * delta0**2, b) * delta0 + ) + + sqrt_val: int256 = 0 + if sqrt_arg > 0: + sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256) + else: + return [self._newton_y(_ANN, _gamma, x, _D, i), 0] + + b_cbrt: int256 = 0 + if b >= 0: + b_cbrt = convert(self._cbrt(convert(b, uint256)), int256) + else: + b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256) + + second_cbrt: int256 = 0 + if delta1 > 0: + # convert(self._cbrt(convert((delta1 + sqrt_val), uint256)/2), int256) + second_cbrt = convert( + self._cbrt(unsafe_div(convert(delta1 + sqrt_val, uint256), 2)), + int256 + ) + else: + second_cbrt = -convert( + self._cbrt(unsafe_div(convert(-(delta1 - sqrt_val), uint256), 2)), + int256 + ) + + # b_cbrt*b_cbrt/10**18*second_cbrt/10**18 + C1: int256 = unsafe_div( + unsafe_div(b_cbrt * b_cbrt, 10**18) * second_cbrt, + 10**18 + ) + + # (b + b*delta0/C1 - C1)/3 + root_K0: int256 = unsafe_div(b + b * delta0 / C1 - C1, 3) + + # D*D/27/x_k*D/x_j*root_K0/a + root: int256 = unsafe_div( + unsafe_div( + unsafe_div(unsafe_div(D * D, 27), x_k) * D, + x_j + ) * root_K0, + a + ) + + out: uint256[2] = [ + convert(root, uint256), + convert(unsafe_div(10**18 * root_K0, a), uint256) + ] + + frac = unsafe_div(out[0] * 10**18, _D) + assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe value for y" + # due to precision issues, get_y can be off by 2 wei or so wrt _newton_y + + return out + + +@internal +@view +def _newton_y( + ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256 +) -> uint256: + + # Calculate x[i] given A, gamma, xp and D using newton's method. + # This is the original method; get_y replaces it, but defaults to + # this version conditionally. + + # We can ignore safuty checks since they are already done in get_y + + frac: uint256 = 0 + for k in range(3): + if k != i: + frac = x[k] * 10**18 / D + assert frac > 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]" + + y: uint256 = D / N_COINS + K0_i: uint256 = 10**18 + S_i: uint256 = 0 + + x_sorted: uint256[N_COINS] = x + x_sorted[i] = 0 + x_sorted = self._sort(x_sorted) # From high to low + + convergence_limit: uint256 = max(max(x_sorted[0] / 10**14, D / 10**14), 100) + + for j in range(2, N_COINS + 1): + _x: uint256 = x_sorted[N_COINS - j] + y = y * D / (_x * N_COINS) # Small _x first + S_i += _x + + for j in range(N_COINS - 1): + K0_i = K0_i * x_sorted[j] * N_COINS / D # Large _x first + + # initialise variables: + diff: uint256 = 0 + y_prev: uint256 = 0 + K0: uint256 = 0 + S: uint256 = 0 + _g1k0: uint256 = 0 + mul1: uint256 = 0 + mul2: uint256 = 0 + yfprime: uint256 = 0 + _dyfprime: uint256 = 0 + fprime: uint256 = 0 + y_minus: uint256 = 0 + y_plus: uint256 = 0 + + for j in range(255): + y_prev = y + + K0 = K0_i * y * N_COINS / D + S = S_i + y + + _g1k0 = gamma + 10**18 + if _g1k0 > K0: + _g1k0 = _g1k0 - K0 + 1 + else: + _g1k0 = K0 - _g1k0 + 1 + + # mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + + # 2*K0 / _g1k0 + mul2 = 10**18 + (2 * 10**18) * K0 / _g1k0 + + yfprime = 10**18 * y + S * mul2 + mul1 + _dyfprime = D * mul2 + if yfprime < _dyfprime: + y = y_prev / 2 + continue + else: + yfprime -= _dyfprime + + fprime = yfprime / y + + # y -= f / f_prime; y = (y * fprime - f) / fprime + y_minus = mul1 / fprime + y_plus = (yfprime + 10**18 * D) / fprime + y_minus * 10**18 / K0 + y_minus += 10**18 * S / fprime + + if y_plus < y_minus: + y = y_prev / 2 + else: + y = y_plus - y_minus + + if y > y_prev: + diff = y - y_prev + else: + diff = y_prev - y + + if diff < max(convergence_limit, y / 10**14): + frac = y * 10**18 / D + assert frac > 10**16 - 1 and frac < 10**20 + 1, "Unsafe value for y" + return y + + raise "Did not converge" + + +@external +@view +def newton_D( + ANN: uint256, + gamma: uint256, + x_unsorted: uint256[N_COINS], + K0_prev: uint256 = 0, +) -> uint256: + """ + @notice Finding the invariant via newtons method using good initial guesses. + @dev ANN is higher by the factor A_MULTIPLIER + @dev ANN is already A * N**N + @param ANN the A * N**N value + @param gamma the gamma value + @param x_unsorted the array of coin balances (not sorted) + @param K0_prev apriori for newton's method derived from get_y_int. Defaults + to zero (no apriori) + """ + x: uint256[N_COINS] = self._sort(x_unsorted) + assert x[0] < max_value(uint256) / 10**18 * N_COINS**N_COINS # dev: out of limits + assert x[0] > 0 # dev: empty pool + + # Safe to do unsafe add since we checked largest x's bounds previously + S: uint256 = unsafe_add(unsafe_add(x[0], x[1]), x[2]) + D: uint256 = 0 + + if K0_prev == 0: + # Geometric mean of 3 numbers cannot be larger than the largest number + # so the following is safe to do: + D = unsafe_mul(N_COINS, self._geometric_mean(x)) + else: + if S > 10**36: + D = self._cbrt( + unsafe_div( + unsafe_div(x[0] * x[1], 10**36) * x[2], + K0_prev + ) * 27 * 10**12 + ) + elif S > 10**24: + D = self._cbrt( + unsafe_div( + unsafe_div(x[0] * x[1], 10**24) * x[2], + K0_prev + ) * 27 * 10**6 + ) + else: + D = self._cbrt( + unsafe_div( + unsafe_div(x[0] * x[1], 10**18) * x[2], + K0_prev + ) * 27 + ) + + # D not zero here if K0_prev > 0, and we checked if x[0] is gt 0. + + # initialise variables: + K0: uint256 = 0 + _g1k0: uint256 = 0 + mul1: uint256 = 0 + mul2: uint256 = 0 + neg_fprime: uint256 = 0 + D_plus: uint256 = 0 + D_minus: uint256 = 0 + D_prev: uint256 = 0 + + diff: uint256 = 0 + frac: uint256 = 0 + + for i in range(255): + + D_prev = D + + # K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D + K0 = unsafe_div( + unsafe_mul( + unsafe_mul( + unsafe_div( + unsafe_mul( + unsafe_mul( + unsafe_div( + unsafe_mul( + unsafe_mul(10**18, x[0]), N_COINS + ), + D, + ), + x[1], + ), + N_COINS, + ), + D, + ), + x[2], + ), + N_COINS, + ), + D, + ) # <-------- We can convert the entire expression using unsafe math. + # since x_i is not too far from D, so overflow is not expected. Also + # D > 0, since we proved that already. unsafe_div is safe. K0 > 0 + # since we can safely assume that D < 10**18 * x[0]. K0 is also + # in the range of 10**18 (it's a property). + + _g1k0 = unsafe_add(gamma, 10**18) # <--------- safe to do unsafe_add. + + if _g1k0 > K0: # The following operations can safely be unsafe. + _g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) + else: + _g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) + + # D / (A * N**N) * _g1k0**2 / gamma**2 + # mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + mul1 = unsafe_div( + unsafe_mul( + unsafe_mul( + unsafe_div( + unsafe_mul( + unsafe_div(unsafe_mul(10**18, D), gamma), _g1k0 + ), + gamma, + ), + _g1k0, + ), + A_MULTIPLIER, + ), + ANN, + ) # <------ Since D > 0, gamma is small, _g1k0 is small, the rest are + # non-zero and small constants, and D has a cap in this method, + # we can safely convert everything to unsafe maths. + + # 2*N*K0 / _g1k0 + # mul2 = (2 * 10**18) * N_COINS * K0 / _g1k0 + mul2 = unsafe_div( + unsafe_mul(2 * 10**18 * N_COINS, K0), _g1k0 + ) # <--------------- K0 is approximately around D, which has a cap of + # 10**15 * 10**18 + 1, since we get that in get_y which is called + # with newton_D. _g1k0 > 0, so the entire expression can be unsafe. + + # neg_fprime: uint256 = (S + S * mul2 / 10**18) + mul1 * N_COINS / K0 - mul2 * D / 10**18 + neg_fprime = unsafe_sub( + unsafe_add( + unsafe_add(S, unsafe_div(unsafe_mul(S, mul2), 10**18)), + unsafe_div(unsafe_mul(mul1, N_COINS), K0), + ), + unsafe_div(unsafe_mul(mul2, D), 10**18), + ) # <--- mul1 is a big number but not huge: safe to unsafely multiply + # with N_coins. neg_fprime > 0 if this expression executes. + # mul2 is in the range of 10**18, since K0 is in that range, S * mul2 + # is safe. The first three sums can be done using unsafe math safely + # and since the final expression will be small since mul2 is small, we + # can safely do the entire expression unsafely. + + # D -= f / fprime + # D * (neg_fprime + S) / neg_fprime + D_plus = unsafe_div(D * unsafe_add(neg_fprime, S), neg_fprime) + + # D*D / neg_fprime + D_minus = unsafe_div(D * D, neg_fprime) + + # Since we know K0 > 0, and neg_fprime > 0, several unsafe operations + # are possible in the following. Also, (10**18 - K0) is safe to mul. + # So the only expressions we keep safe are (D_minus + ...) and (D * ...) + if 10**18 > K0: + # D_minus += D * (mul1 / neg_fprime) / 10**18 * (10**18 - K0) / K0 + D_minus += unsafe_div( + unsafe_mul( + unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18), + unsafe_sub(10**18, K0), + ), + K0, + ) + else: + # D_minus -= D * (mul1 / neg_fprime) / 10**18 * (K0 - 10**18) / K0 + D_minus -= unsafe_div( + unsafe_mul( + unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18), + unsafe_sub(K0, 10**18), + ), + K0, + ) + + if D_plus > D_minus: + D = unsafe_sub(D_plus, D_minus) # <--------- Safe since we check. + else: + D = unsafe_div(unsafe_sub(D_minus, D_plus), 2) + + if D > D_prev: + diff = unsafe_sub(D, D_prev) + else: + diff = unsafe_sub(D_prev, D) + + # Could reduce precision for gas efficiency here: + if unsafe_mul(diff, 10**14) < max(10**16, D): + + # Test that we are safe with the next get_y + for _x in x: + frac = unsafe_div(unsafe_mul(_x, 10**18), D) + assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]" + + return D + raise "Did not converge" + + +@external +@view +def get_p( + _xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[N_COINS-1] +) -> uint256[N_COINS-1]: + """ + @notice Calculates dx/dy. + @dev Output needs to be multiplied with price_scale to get the actual value. + @param _xp Balances of the pool. + @param _D Current value of D. + @param _A_gamma Amplification coefficient and gamma. + """ + + assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe D values + + # K0 = P * N**N / D**N. + # K0 is dimensionless and has 10**36 precision: + K0: uint256 = unsafe_div( + unsafe_div(unsafe_div(27 * _xp[0] * _xp[1], _D) * _xp[2], _D) * 10**36, + _D + ) + + # GK0 is in 10**36 precision and is dimensionless. + # GK0 = ( + # 2 * _K0 * _K0 / 10**36 * _K0 / 10**36 + # + (gamma + 10**18)**2 + # - (_K0 * _K0 / 10**36 * (2 * gamma + 3 * 10**18) / 10**18) + # ) + # GK0 is always positive. So the following should never revert: + GK0: uint256 = ( + unsafe_div(unsafe_div(2 * K0 * K0, 10**36) * K0, 10**36) + + pow_mod256(unsafe_add(_A_gamma[1], 10**18), 2) + - unsafe_div( + unsafe_div(pow_mod256(K0, 2), 10**36) * unsafe_add(unsafe_mul(2, _A_gamma[1]), 3 * 10**18), + 10**18 + ) + ) + + # NNAG2 = N**N * A * gamma**2 + NNAG2: uint256 = unsafe_div(unsafe_mul(_A_gamma[0], pow_mod256(_A_gamma[1], 2)), A_MULTIPLIER) + + # denominator = (GK0 + NNAG2 * x / D * _K0 / 10**36) + denominator: uint256 = (GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[0], _D) * K0, 10**36) ) + + # p_xy = x * (GK0 + NNAG2 * y / D * K0 / 10**36) / y * 10**18 / denominator + # p_xz = x * (GK0 + NNAG2 * z / D * K0 / 10**36) / z * 10**18 / denominator + # p is in 10**18 precision. + return [ + unsafe_div( + _xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[1], _D) * K0, 10**36) ) / _xp[1] * 10**18, + denominator + ), + unsafe_div( + _xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[2], _D) * K0, 10**36) ) / _xp[2] * 10**18, + denominator + ), + ] + + +# --------------------------- Math Utils ------------------------------------- + + +@external +@view +def cbrt(x: uint256) -> uint256: + """ + @notice Calculate the cubic root of a number in 1e18 precision + @dev Consumes around 1500 gas units + @param x The number to calculate the cubic root of + """ + return self._cbrt(x) + + +@external +@view +def geometric_mean(_x: uint256[3]) -> uint256: + """ + @notice Calculate the geometric mean of a list of numbers in 1e18 precision. + @param _x list of 3 numbers to sort + """ + return self._geometric_mean(_x) + + +@external +@view +def reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256: + """ + @notice Calculates the reduction coefficient for the given x and fee_gamma + @dev This method is used for calculating fees. + @param x The x values + @param fee_gamma The fee gamma value + """ + return self._reduction_coefficient(x, fee_gamma) + + +@external +@view +def wad_exp(_power: int256) -> uint256: + """ + @notice Calculates the e**x with 1e18 precision + @param _power The number to calculate the exponential of + """ + return self._snekmate_wad_exp(_power) + + +@internal +@pure +def _reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256: + + # fee_gamma / (fee_gamma + (1 - K)) + # where + # K = prod(x) / (sum(x) / N)**N + # (all normalized to 1e18) + + S: uint256 = x[0] + x[1] + x[2] + + # Could be good to pre-sort x, but it is used only for dynamic fee + K: uint256 = 10**18 * N_COINS * x[0] / S + K = unsafe_div(K * N_COINS * x[1], S) # <- unsafe div is safu. + K = unsafe_div(K * N_COINS * x[2], S) + + if fee_gamma > 0: + K = fee_gamma * 10**18 / (fee_gamma + 10**18 - K) + + return K + + +@internal +@pure +def _snekmate_wad_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 +@pure +def _snekmate_log_2(x: uint256, roundup: bool) -> uint256: + """ + @notice An `internal` helper function that returns the log in base 2 + of `x`, following the selected rounding direction. + @dev This implementation is derived from Snekmate, which is authored + by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license. + https://github.com/pcaversaccio/snekmate + @dev Note that it returns 0 if given 0. The implementation is + inspired by OpenZeppelin's implementation here: + https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol. + @param x The 32-byte variable. + @param roundup The Boolean variable that specifies whether + to round up or not. The default `False` is round down. + @return uint256 The 32-byte calculation result. + """ + value: uint256 = x + result: uint256 = empty(uint256) + + # The following lines cannot overflow because we have the well-known + # decay behaviour of `log_2(max_value(uint256)) < max_value(uint256)`. + if x >> 128 != empty(uint256): + value = x >> 128 + result = 128 + if value >> 64 != empty(uint256): + value = value >> 64 + result = unsafe_add(result, 64) + if value >> 32 != empty(uint256): + value = value >> 32 + result = unsafe_add(result, 32) + if value >> 16 != empty(uint256): + value = value >> 16 + result = unsafe_add(result, 16) + if value >> 8 != empty(uint256): + value = value >> 8 + result = unsafe_add(result, 8) + if value >> 4 != empty(uint256): + value = value >> 4 + result = unsafe_add(result, 4) + if value >> 2 != empty(uint256): + value = value >> 2 + result = unsafe_add(result, 2) + if value >> 1 != empty(uint256): + result = unsafe_add(result, 1) + + if (roundup and (1 << result) < x): + result = unsafe_add(result, 1) + + return result + + +@internal +@pure +def _cbrt(x: uint256) -> uint256: + + xx: uint256 = 0 + if x >= 115792089237316195423570985008687907853269 * 10**18: + xx = x + elif x >= 115792089237316195423570985008687907853269: + xx = unsafe_mul(x, 10**18) + else: + xx = unsafe_mul(x, 10**36) + + log2x: int256 = convert(self._snekmate_log_2(xx, False), int256) + + # When we divide log2x by 3, the remainder is (log2x % 3). + # So if we just multiply 2**(log2x/3) and discard the remainder to calculate our + # guess, the newton method will need more iterations to converge to a solution, + # since it is missing that precision. It's a few more calculations now to do less + # calculations later: + # pow = log2(x) // 3 + # remainder = log2(x) % 3 + # initial_guess = 2 ** pow * cbrt(2) ** remainder + # substituting -> 2 = 1.26 ≈ 1260 / 1000, we get: + # + # initial_guess = 2 ** pow * 1260 ** remainder // 1000 ** remainder + + remainder: uint256 = convert(log2x, uint256) % 3 + a: uint256 = unsafe_div( + unsafe_mul( + pow_mod256(2, unsafe_div(convert(log2x, uint256), 3)), # <- pow + pow_mod256(1260, remainder), + ), + pow_mod256(1000, remainder), + ) + + # Because we chose good initial values for cube roots, 7 newton raphson iterations + # are just about sufficient. 6 iterations would result in non-convergences, and 8 + # would be one too many iterations. Without initial values, the iteration count + # can go up to 20 or greater. The iterations are unrolled. This reduces gas costs + # but takes up more bytecode: + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + + if x >= 115792089237316195423570985008687907853269 * 10**18: + a = unsafe_mul(a, 10**12) + elif x >= 115792089237316195423570985008687907853269: + a = unsafe_mul(a, 10**6) + + return a + + +@internal +@pure +def _sort(unsorted_x: uint256[3]) -> uint256[3]: + + # Sorts a three-array number in a descending order: + + x: uint256[N_COINS] = unsorted_x + temp_var: uint256 = x[0] + if x[0] < x[1]: + x[0] = x[1] + x[1] = temp_var + if x[0] < x[2]: + temp_var = x[0] + x[0] = x[2] + x[2] = temp_var + if x[1] < x[2]: + temp_var = x[1] + x[1] = x[2] + x[2] = temp_var + + return x + + +@internal +@view +def _geometric_mean(_x: uint256[3]) -> uint256: + + # calculates a geometric mean for three numbers. + + prod: uint256 = unsafe_div( + unsafe_div(_x[0] * _x[1], 10**18) * _x[2], + 10**18 + ) + + if prod == 0: + return 0 + + return self._cbrt(prod) diff --git a/contracts/zksync/CurveCryptoViews3Optimized.vy b/contracts/zksync/CurveCryptoViews3Optimized.vy new file mode 100644 index 0000000..1b963d5 --- /dev/null +++ b/contracts/zksync/CurveCryptoViews3Optimized.vy @@ -0,0 +1,414 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveCryptoViews3Optimized +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice This contract contains view-only external methods which can be + gas-inefficient when called from smart contracts. +""" + +from vyper.interfaces import ERC20 + + +interface Curve: + def MATH() -> Math: view + def A() -> uint256: view + def gamma() -> uint256: view + def price_scale(i: uint256) -> uint256: view + def price_oracle(i: uint256) -> uint256: view + def get_virtual_price() -> uint256: view + def balances(i: uint256) -> uint256: view + def D() -> uint256: view + def fee_calc(xp: uint256[N_COINS]) -> uint256: view + def calc_token_fee( + amounts: uint256[N_COINS], xp: uint256[N_COINS] + ) -> uint256: view + def future_A_gamma_time() -> uint256: view + def totalSupply() -> uint256: view + def precisions() -> uint256[N_COINS]: view + def packed_fee_params() -> uint256: view + + +interface Math: + def newton_D( + ANN: uint256, + gamma: uint256, + x_unsorted: uint256[N_COINS], + K0_prev: uint256 + ) -> uint256: view + def get_y( + ANN: uint256, + gamma: uint256, + x: uint256[N_COINS], + D: uint256, + i: uint256, + ) -> uint256[2]: view + def cbrt(x: uint256) -> uint256: view + def reduction_coefficient( + x: uint256[N_COINS], fee_gamma: uint256 + ) -> uint256: view + + +N_COINS: constant(uint256) = 3 +PRECISION: constant(uint256) = 10**18 + + +@external +@view +def get_dy( + i: uint256, j: uint256, dx: uint256, swap: address +) -> uint256: + + dy: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + + # dy = (get_y(x + dx) - y) * (1 - fee) + dy, xp = self._get_dy_nofee(i, j, dx, swap) + dy -= Curve(swap).fee_calc(xp) * dy / 10**10 + + return dy + + +@view +@external +def get_dx( + i: uint256, j: uint256, dy: uint256, swap: address +) -> uint256: + + dx: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + fee_dy: uint256 = 0 + _dy: uint256 = dy + + # for more precise dx (but never exact), increase num loops + for k in range(5): + dx, xp = self._get_dx_fee(i, j, _dy, swap) + fee_dy = Curve(swap).fee_calc(xp) * _dy / 10**10 + _dy = dy + fee_dy + 1 + + return dx + + +@view +@external +def calc_withdraw_one_coin( + token_amount: uint256, i: uint256, swap: address +) -> uint256: + + return self._calc_withdraw_one_coin(token_amount, i, swap)[0] + + +@view +@external +def calc_token_amount( + amounts: uint256[N_COINS], deposit: bool, swap: address +) -> uint256: + + d_token: uint256 = 0 + amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + + d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap) + d_token -= ( + Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1 + ) + + return d_token + + +@external +@view +def calc_fee_get_dy(i: uint256, j: uint256, dx: uint256, swap: address +) -> uint256: + + dy: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + dy, xp = self._get_dy_nofee(i, j, dx, swap) + + return Curve(swap).fee_calc(xp) * dy / 10**10 + + +@external +@view +def calc_fee_withdraw_one_coin( + token_amount: uint256, i: uint256, swap: address +) -> uint256: + + return self._calc_withdraw_one_coin(token_amount, i, swap)[1] + + +@view +@external +def calc_fee_token_amount( + amounts: uint256[N_COINS], deposit: bool, swap: address +) -> uint256: + + d_token: uint256 = 0 + amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap) + + return Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1 + + +@internal +@view +def _calc_D_ramp( + A: uint256, + gamma: uint256, + xp: uint256[N_COINS], + precisions: uint256[N_COINS], + price_scale: uint256[N_COINS - 1], + swap: address +) -> uint256: + + math: Math = Curve(swap).MATH() + + D: uint256 = Curve(swap).D() + if Curve(swap).future_A_gamma_time() > block.timestamp: + _xp: uint256[N_COINS] = xp + _xp[0] *= precisions[0] + for k in range(N_COINS - 1): + _xp[k + 1] = ( + _xp[k + 1] * price_scale[k] * precisions[k + 1] / PRECISION + ) + D = math.newton_D(A, gamma, _xp, 0) + + return D + + +@internal +@view +def _get_dx_fee( + i: uint256, j: uint256, dy: uint256, swap: address +) -> (uint256, uint256[N_COINS]): + + # here, dy must include fees (and 1 wei offset) + + assert i != j and i < N_COINS and j < N_COINS, "coin index out of range" + assert dy > 0, "do not exchange out 0 coins" + + math: Math = Curve(swap).MATH() + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + precisions: uint256[N_COINS] = empty(uint256[N_COINS]) + price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + D: uint256 = 0 + token_supply: uint256 = 0 + A: uint256 = 0 + gamma: uint256 = 0 + + xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap) + + # adjust xp with output dy. dy contains fee element, which we handle later + # (hence this internal method is called _get_dx_fee) + xp[j] -= dy + xp[0] *= precisions[0] + for k in range(N_COINS - 1): + xp[k + 1] = xp[k + 1] * price_scale[k] * precisions[k + 1] / PRECISION + + x_out: uint256[2] = math.get_y(A, gamma, xp, D, i) + dx: uint256 = x_out[0] - xp[i] + xp[i] = x_out[0] + if i > 0: + dx = dx * PRECISION / price_scale[i - 1] + dx /= precisions[i] + + return dx, xp + + +@internal +@view +def _get_dy_nofee( + i: uint256, j: uint256, dx: uint256, swap: address +) -> (uint256, uint256[N_COINS]): + + assert i != j and i < N_COINS and j < N_COINS, "coin index out of range" + assert dx > 0, "do not exchange 0 coins" + + math: Math = Curve(swap).MATH() + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + precisions: uint256[N_COINS] = empty(uint256[N_COINS]) + price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + D: uint256 = 0 + token_supply: uint256 = 0 + A: uint256 = 0 + gamma: uint256 = 0 + + xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap) + + # adjust xp with input dx + xp[i] += dx + xp[0] *= precisions[0] + for k in range(N_COINS - 1): + xp[k + 1] = xp[k + 1] * price_scale[k] * precisions[k + 1] / PRECISION + + y_out: uint256[2] = math.get_y(A, gamma, xp, D, j) + dy: uint256 = xp[j] - y_out[0] - 1 + xp[j] = y_out[0] + if j > 0: + dy = dy * PRECISION / price_scale[j - 1] + dy /= precisions[j] + + return dy, xp + + +@internal +@view +def _calc_dtoken_nofee( + amounts: uint256[N_COINS], deposit: bool, swap: address +) -> (uint256, uint256[N_COINS], uint256[N_COINS]): + + math: Math = Curve(swap).MATH() + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + precisions: uint256[N_COINS] = empty(uint256[N_COINS]) + price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + D0: uint256 = 0 + token_supply: uint256 = 0 + A: uint256 = 0 + gamma: uint256 = 0 + + xp, D0, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap) + + amountsp: uint256[N_COINS] = amounts + if deposit: + for k in range(N_COINS): + xp[k] += amounts[k] + else: + for k in range(N_COINS): + xp[k] -= amounts[k] + + xp[0] *= precisions[0] + amountsp[0] *= precisions[0] + for k in range(N_COINS - 1): + p: uint256 = price_scale[k] * precisions[k + 1] + xp[k + 1] = xp[k + 1] * p / PRECISION + amountsp[k + 1] = amountsp[k + 1] * p / PRECISION + + D: uint256 = math.newton_D(A, gamma, xp, 0) + d_token: uint256 = token_supply * D / D0 + + if deposit: + d_token -= token_supply + else: + d_token = token_supply - d_token + + return d_token, amountsp, xp + + +@internal +@view +def _calc_withdraw_one_coin( + token_amount: uint256, + i: uint256, + swap: address +) -> (uint256, uint256): + + token_supply: uint256 = Curve(swap).totalSupply() + assert token_amount <= token_supply # dev: token amount more than supply + assert i < N_COINS # dev: coin out of range + + math: Math = Curve(swap).MATH() + + xx: uint256[N_COINS] = empty(uint256[N_COINS]) + price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + for k in range(N_COINS): + xx[k] = Curve(swap).balances(k) + if k > 0: + price_scale[k - 1] = Curve(swap).price_scale(k - 1) + + precisions: uint256[N_COINS] = Curve(swap).precisions() + A: uint256 = Curve(swap).A() + gamma: uint256 = Curve(swap).gamma() + xp: uint256[N_COINS] = precisions + D0: uint256 = 0 + p: uint256 = 0 + + price_scale_i: uint256 = PRECISION * precisions[0] + xp[0] *= xx[0] + for k in range(1, N_COINS): + + p = price_scale[k-1] + if i == k: + price_scale_i = p * xp[i] + xp[k] = xp[k] * xx[k] * p / PRECISION + + if Curve(swap).future_A_gamma_time() > block.timestamp: + D0 = math.newton_D(A, gamma, xp, 0) + else: + D0 = Curve(swap).D() + + D: uint256 = D0 + + fee: uint256 = self._fee(xp, swap) + dD: uint256 = token_amount * D / token_supply + + D_fee: uint256 = fee * dD / (2 * 10**10) + 1 + approx_fee: uint256 = N_COINS * D_fee * xx[i] / D + + D -= (dD - D_fee) + + y_out: uint256[2] = math.get_y(A, gamma, xp, D, i) + dy: uint256 = (xp[i] - y_out[0]) * PRECISION / price_scale_i + xp[i] = y_out[0] + + return dy, approx_fee + + +@internal +@view +def _fee(xp: uint256[N_COINS], swap: address) -> uint256: + math: Math = Curve(swap).MATH() + packed_fee_params: uint256 = Curve(swap).packed_fee_params() + fee_params: uint256[3] = self._unpack(packed_fee_params) + f: uint256 = math.reduction_coefficient(xp, fee_params[2]) + return (fee_params[0] * f + fee_params[1] * (10**18 - f)) / 10**18 + + +@internal +@view +def _prep_calc(swap: address) -> ( + uint256[N_COINS], + uint256, + uint256, + uint256[N_COINS-1], + uint256, + uint256, + uint256[N_COINS] +): + + precisions: uint256[N_COINS] = Curve(swap).precisions() + token_supply: uint256 = Curve(swap).totalSupply() + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + for k in range(N_COINS): + xp[k] = Curve(swap).balances(k) + + price_scale: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1]) + for k in range(N_COINS - 1): + price_scale[k] = Curve(swap).price_scale(k) + + A: uint256 = Curve(swap).A() + gamma: uint256 = Curve(swap).gamma() + D: uint256 = self._calc_D_ramp( + A, gamma, xp, precisions, price_scale, swap + ) + + return xp, D, token_supply, price_scale, A, gamma, precisions + + +@internal +@view +def _unpack(_packed: uint256) -> uint256[3]: + """ + @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) + @param val The uint256 to unpack + @return The unpacked uint256[3] + """ + return [ + (_packed >> 128) & 18446744073709551615, + (_packed >> 64) & 18446744073709551615, + _packed & 18446744073709551615, + ] diff --git a/contracts/zksync/CurveL2TricryptoFactory.vy b/contracts/zksync/CurveL2TricryptoFactory.vy new file mode 100644 index 0000000..d5cc383 --- /dev/null +++ b/contracts/zksync/CurveL2TricryptoFactory.vy @@ -0,0 +1,461 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveL2TricryptoFactory +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Permissionless 3-coin cryptoswap pool deployer and registry +""" + +interface TricryptoPool: + def balances(i: uint256) -> uint256: view + +interface ERC20: + def decimals() -> uint256: view + + +event TricryptoPoolDeployed: + pool: address + name: String[64] + symbol: String[32] + weth: address + coins: address[N_COINS] + math: address + salt: bytes32 + packed_precisions: uint256 + packed_A_gamma: uint256 + packed_fee_params: uint256 + packed_rebalancing_params: uint256 + packed_prices: uint256 + deployer: address + +event UpdateFeeReceiver: + _old_fee_receiver: address + _new_fee_receiver: address + +event UpdatePoolImplementation: + _implemention_id: uint256 + _old_pool_implementation: address + _new_pool_implementation: address + +event UpdateMathImplementation: + _old_math_implementation: address + _new_math_implementation: address + +event UpdateViewsImplementation: + _old_views_implementation: address + _new_views_implementation: address + +event TransferOwnership: + _old_owner: address + _new_owner: address + + +struct PoolArray: + coins: address[N_COINS] + decimals: uint256[N_COINS] + implementation: address + + +N_COINS: constant(uint256) = 3 +A_MULTIPLIER: constant(uint256) = 10000 + +# Limits +MAX_FEE: constant(uint256) = 10 * 10 ** 9 + +MIN_GAMMA: constant(uint256) = 10 ** 10 +MAX_GAMMA: constant(uint256) = 5 * 10**16 + +MIN_A: constant(uint256) = N_COINS ** N_COINS * A_MULTIPLIER / 100 +MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS + +PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1) +PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1 + +admin: public(address) +future_admin: public(address) + +# fee receiver for all pools: +fee_receiver: public(address) + +pool_implementations: public(HashMap[uint256, address]) +views_implementation: public(address) +math_implementation: public(address) + +# mapping of coins -> pools for trading +# a mapping key is generated for each pair of addresses via +# `bitwise_xor(convert(a, uint256), convert(b, uint256))` +markets: HashMap[uint256, address[4294967296]] +market_counts: HashMap[uint256, uint256] + +pool_count: public(uint256) # actual length of pool_list +pool_data: HashMap[address, PoolArray] +pool_list: public(address[4294967296]) # master list of pools + + +@external +def __init__(_fee_receiver: address, _admin: address): + + self.fee_receiver = _fee_receiver + self.admin = _admin + + log UpdateFeeReceiver(empty(address), _fee_receiver) + log TransferOwnership(empty(address), _admin) + + +@internal +@view +def _pack(x: uint256[3]) -> uint256: + """ + @notice Packs 3 integers with values <= 10**18 into a uint256 + @param x The uint256[3] to pack + @return The packed uint256 + """ + return (x[0] << 128) | (x[1] << 64) | x[2] + + + +# <--- Pool Deployers ---> + +@external +def deploy_pool( + _name: String[64], + _symbol: String[32], + _coins: address[N_COINS], + _weth: address, + implementation_id: uint256, + A: uint256, + gamma: uint256, + mid_fee: uint256, + out_fee: uint256, + fee_gamma: uint256, + allowed_extra_profit: uint256, + adjustment_step: uint256, + ma_exp_time: uint256, + initial_prices: uint256[N_COINS-1], +) -> address: + """ + @notice Deploy a new pool + @param _name Name of the new plain pool + @param _symbol Symbol for the new plain pool - will be concatenated with factory symbol + + @return Address of the deployed pool + """ + pool_implementation: address = self.pool_implementations[implementation_id] + assert pool_implementation != empty(address), "Pool implementation not set" + + # Validate parameters + assert A > MIN_A-1 + assert A < MAX_A+1 + + assert gamma > MIN_GAMMA-1 + assert gamma < MAX_GAMMA+1 + + assert mid_fee < MAX_FEE-1 # mid_fee can be zero + assert out_fee >= mid_fee + assert out_fee < MAX_FEE-1 + assert fee_gamma < 10**18+1 + assert fee_gamma > 0 + + assert allowed_extra_profit < 10**18+1 + + assert adjustment_step < 10**18+1 + assert adjustment_step > 0 + + assert ma_exp_time < 872542 # 7 * 24 * 60 * 60 / ln(2) + assert ma_exp_time > 86 # 60 / ln(2) + + assert min(initial_prices[0], initial_prices[1]) > 10**6 + assert max(initial_prices[0], initial_prices[1]) < 10**30 + + assert _coins[0] != _coins[1] and _coins[1] != _coins[2] and _coins[0] != _coins[2], "Duplicate coins" + + decimals: uint256[N_COINS] = empty(uint256[N_COINS]) + precisions: uint256[N_COINS] = empty(uint256[N_COINS]) + for i in range(N_COINS): + d: uint256 = ERC20(_coins[i]).decimals() + assert d < 19, "Max 18 decimals for coins" + decimals[i] = d + precisions[i] = 10** (18 - d) + + # pack precisions + packed_precisions: uint256 = self._pack(precisions) + + # pack fees + packed_fee_params: uint256 = self._pack( + [mid_fee, out_fee, fee_gamma] + ) + + # pack liquidity rebalancing params + packed_rebalancing_params: uint256 = self._pack( + [allowed_extra_profit, adjustment_step, ma_exp_time] + ) + + # pack A_gamma + packed_A_gamma: uint256 = A << 128 + packed_A_gamma = packed_A_gamma | gamma + + # pack initial prices + packed_prices: uint256 = 0 + for k in range(N_COINS - 1): + packed_prices = packed_prices << PRICE_SIZE + p: uint256 = initial_prices[N_COINS - 2 - k] + assert p < PRICE_MASK + packed_prices = p | packed_prices + + # pool is an ERC20 implementation + _salt: bytes32 = block.prevhash + _math_implementation: address = self.math_implementation + pool: address = create_from_blueprint( + pool_implementation, + _name, + _symbol, + _coins, + _math_implementation, + _weth, + _salt, + packed_precisions, + packed_A_gamma, + packed_fee_params, + packed_rebalancing_params, + packed_prices, + code_offset=3 + ) + + # populate pool data + length: uint256 = self.pool_count + self.pool_list[length] = pool + self.pool_count = length + 1 + self.pool_data[pool].decimals = decimals + self.pool_data[pool].coins = _coins + self.pool_data[pool].implementation = pool_implementation + + # add coins to market: + self._add_coins_to_market(_coins[0], _coins[1], pool) + self._add_coins_to_market(_coins[0], _coins[2], pool) + self._add_coins_to_market(_coins[1], _coins[2], pool) + + log TricryptoPoolDeployed( + pool, + _name, + _symbol, + _weth, + _coins, + _math_implementation, + _salt, + packed_precisions, + packed_A_gamma, + packed_fee_params, + packed_rebalancing_params, + packed_prices, + msg.sender, + ) + + return pool + + +@internal +def _add_coins_to_market(coin_a: address, coin_b: address, pool: address): + + key: uint256 = ( + convert(coin_a, uint256) ^ convert(coin_b, uint256) + ) + + length: uint256 = self.market_counts[key] + self.markets[key][length] = pool + self.market_counts[key] = length + 1 + + +# <--- Admin / Guarded Functionality ---> + + +@external +def set_fee_receiver(_fee_receiver: address): + """ + @notice Set fee receiver + @param _fee_receiver Address that fees are sent to + """ + assert msg.sender == self.admin, "dev: admin only" + + log UpdateFeeReceiver(self.fee_receiver, _fee_receiver) + self.fee_receiver = _fee_receiver + + +@external +def set_pool_implementation( + _pool_implementation: address, _implementation_index: uint256 +): + """ + @notice Set pool implementation + @dev Set to empty(address) to prevent deployment of new pools + @param _pool_implementation Address of the new pool implementation + @param _implementation_index Index of the pool implementation + """ + assert msg.sender == self.admin, "dev: admin only" + + log UpdatePoolImplementation( + _implementation_index, + self.pool_implementations[_implementation_index], + _pool_implementation + ) + + self.pool_implementations[_implementation_index] = _pool_implementation + + +@external +def set_views_implementation(_views_implementation: address): + """ + @notice Set views contract implementation + @param _views_implementation Address of the new views contract + """ + assert msg.sender == self.admin, "dev: admin only" + + log UpdateViewsImplementation(self.views_implementation, _views_implementation) + self.views_implementation = _views_implementation + + +@external +def set_math_implementation(_math_implementation: address): + """ + @notice Set math implementation + @param _math_implementation Address of the new math contract + """ + assert msg.sender == self.admin, "dev: admin only" + + log UpdateMathImplementation(self.math_implementation, _math_implementation) + self.math_implementation = _math_implementation + + +@external +def commit_transfer_ownership(_addr: address): + """ + @notice Transfer ownership of this contract to `addr` + @param _addr Address of the new owner + """ + assert msg.sender == self.admin, "dev: admin only" + + self.future_admin = _addr + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + @dev Only callable by the new owner + """ + assert msg.sender == self.future_admin, "dev: future admin only" + + log TransferOwnership(self.admin, msg.sender) + self.admin = msg.sender + + +# <--- Factory Getters ---> + + +@view +@external +def get_implementation_address(_pool: address) -> address: + """ + @notice Get the address of the implementation contract used for a factory pool + @param _pool Pool address + @return Implementation contract address + """ + return self.pool_data[_pool].implementation + + +@view +@external +def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: + """ + @notice Find an available pool for exchanging two coins + @param _from Address of coin to be sent + @param _to Address of coin to be received + @param i Index value. When multiple pools are available + this value is used to return the n'th address. + @return Pool address + """ + key: uint256 = convert(_from, uint256) ^ convert(_to, uint256) + return self.markets[key][i] + + +# <--- Pool Getters ---> + + +@view +@external +def get_coins(_pool: address) -> address[N_COINS]: + """ + @notice Get the coins within a pool + @param _pool Pool address + @return List of coin addresses + """ + return self.pool_data[_pool].coins + + +@view +@external +def get_decimals(_pool: address) -> uint256[N_COINS]: + """ + @notice Get decimal places for each coin within a pool + @param _pool Pool address + @return uint256 list of decimals + """ + return self.pool_data[_pool].decimals + + +@view +@external +def get_balances(_pool: address) -> uint256[N_COINS]: + """ + @notice Get balances for each coin within a pool + @dev For pools using lending, these are the wrapped coin balances + @param _pool Pool address + @return uint256 list of balances + """ + return [ + TricryptoPool(_pool).balances(0), + TricryptoPool(_pool).balances(1), + TricryptoPool(_pool).balances(2), + ] + + +@view +@external +def get_coin_indices( + _pool: address, + _from: address, + _to: address +) -> (uint256, uint256): + """ + @notice Convert coin addresses to indices for use with pool methods + @param _pool Pool address + @param _from Coin address to be used as `i` within a pool + @param _to Coin address to be used as `j` within a pool + @return uint256 `i`, uint256 `j` + """ + coins: address[N_COINS] = self.pool_data[_pool].coins + + for i in range(N_COINS): + for j in range(N_COINS): + if i == j: + continue + + if coins[i] == _from and coins[j] == _to: + return i, j + + raise "Coins not found" + + +@view +@external +def get_market_counts(coin_a: address, coin_b: address) -> uint256: + """ + @notice Gets the number of markets with the specified coins. + @return Number of pools with the input coins + """ + + key: uint256 = ( + convert(coin_a, uint256) ^ convert(coin_b, uint256) + ) + + return self.market_counts[key] diff --git a/contracts/zksync/CurveTricryptoOptimized.vy b/contracts/zksync/CurveTricryptoOptimized.vy new file mode 100644 index 0000000..52da406 --- /dev/null +++ b/contracts/zksync/CurveTricryptoOptimized.vy @@ -0,0 +1,2080 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveTricryptoOptimized +@author Curve.Fi +@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. +""" + +from vyper.interfaces import ERC20 +implements: ERC20 # <--------------------- AMM contract is also the LP token. + +# --------------------------------- Interfaces ------------------------------- + +interface Math: + def geometric_mean(_x: uint256[N_COINS]) -> uint256: view + def wad_exp(_power: int256) -> uint256: view + def cbrt(x: uint256) -> uint256: view + def reduction_coefficient( + x: uint256[N_COINS], fee_gamma: uint256 + ) -> uint256: view + def newton_D( + ANN: uint256, + gamma: uint256, + x_unsorted: uint256[N_COINS], + K0_prev: uint256 + ) -> uint256: view + def get_y( + ANN: uint256, + gamma: uint256, + x: uint256[N_COINS], + D: uint256, + i: uint256, + ) -> uint256[2]: view + def get_p( + _xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[2], + ) -> uint256[N_COINS-1]: view + +interface Factory: + def admin() -> address: view + def fee_receiver() -> address: view + def views_implementation() -> address: view + +interface Views: + def calc_token_amount( + amounts: uint256[N_COINS], deposit: bool, swap: address + ) -> uint256: view + def get_dy( + i: uint256, j: uint256, dx: uint256, swap: address + ) -> uint256: view + def get_dx( + i: uint256, j: uint256, dy: uint256, swap: address + ) -> uint256: view + + +# ------------------------------- Events ------------------------------------- + +event Transfer: + sender: indexed(address) + receiver: indexed(address) + value: uint256 + +event Approval: + owner: indexed(address) + spender: indexed(address) + value: uint256 + +event TokenExchange: + buyer: indexed(address) + sold_id: uint256 + tokens_sold: uint256 + bought_id: uint256 + tokens_bought: uint256 + fee: uint256 + packed_price_scale: uint256 + +event AddLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fee: uint256 + token_supply: uint256 + packed_price_scale: uint256 + +event RemoveLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + token_supply: uint256 + +event RemoveLiquidityOne: + provider: indexed(address) + token_amount: uint256 + coin_index: uint256 + coin_amount: uint256 + approx_fee: uint256 + packed_price_scale: uint256 + +event NewParameters: + mid_fee: uint256 + out_fee: uint256 + fee_gamma: uint256 + allowed_extra_profit: uint256 + adjustment_step: uint256 + ma_time: uint256 + xcp_ma_time: uint256 + +event RampAgamma: + initial_A: uint256 + future_A: uint256 + initial_gamma: uint256 + future_gamma: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + current_A: uint256 + current_gamma: uint256 + time: uint256 + +event ClaimAdminFee: + admin: indexed(address) + tokens: uint256[N_COINS] + + +# ----------------------- Storage/State Variables ---------------------------- + +N_COINS: constant(uint256) = 3 +PRECISION: constant(uint256) = 10**18 # <------- The precision to convert to. +PRECISIONS: immutable(uint256[N_COINS]) + +MATH: public(immutable(Math)) +coins: public(immutable(address[N_COINS])) +factory: public(immutable(Factory)) + +price_scale_packed: uint256 # <------------------------ Internal price scale. +price_oracle_packed: uint256 # <------- Price target given by moving average. +cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. + +last_prices_packed: uint256 +last_timestamp: public(uint256) # idx 0 is for prices, idx 1 is for xcp. +last_xcp: public(uint256) +xcp_ma_time: public(uint256) + +initial_A_gamma: public(uint256) +initial_A_gamma_time: public(uint256) + +future_A_gamma: public(uint256) +future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. +# This value is 0 (default) when pool is first deployed, and only gets +# populated by block.timestamp + future_time in `ramp_A_gamma` when the +# ramping process is initiated. After ramping is finished +# (i.e. self.future_A_gamma_time < block.timestamp), the variable is left +# and not set to 0. + +balances: public(uint256[N_COINS]) +D: public(uint256) +xcp_profit: public(uint256) +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 : +packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing +# parameters allowed_extra_profit, adjustment_step, and ma_time. + +# Fee params that determine dynamic fees: +packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma. + +ADMIN_FEE: public(constant(uint256)) = 5 * 10**9 # <----- 50% of earned fees. +MIN_FEE: constant(uint256) = 5 * 10**5 # <-------------------------- 0.5 BPS. +MAX_FEE: constant(uint256) = 10 * 10**9 +NOISE_FEE: constant(uint256) = 10**5 # <---------------------------- 0.1 BPS. + +# ----------------------- Admin params --------------------------------------- + +last_admin_fee_claim_timestamp: uint256 +admin_lp_virtual_balance: uint256 + +MIN_RAMP_TIME: constant(uint256) = 86400 +MIN_ADMIN_FEE_CLAIM_INTERVAL: constant(uint256) = 86400 + +A_MULTIPLIER: constant(uint256) = 10000 +MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 100 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 +MAX_A_CHANGE: constant(uint256) = 10 +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA: constant(uint256) = 5 * 10**16 + +PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1) +PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1 + +# ----------------------- ERC20 Specific vars -------------------------------- + +name: public(immutable(String[64])) +symbol: public(immutable(String[32])) +decimals: public(constant(uint8)) = 18 +version: public(constant(String[8])) = "v2.0.0" + +balanceOf: public(HashMap[address, uint256]) +allowance: public(HashMap[address, HashMap[address, uint256]]) +totalSupply: public(uint256) +nonces: public(HashMap[address, uint256]) + +EIP712_TYPEHASH: constant(bytes32) = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" +) +EIP2612_TYPEHASH: constant(bytes32) = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +) +VERSION_HASH: constant(bytes32) = keccak256(version) +NAME_HASH: immutable(bytes32) +CACHED_CHAIN_ID: immutable(uint256) +salt: public(immutable(bytes32)) +CACHED_DOMAIN_SEPARATOR: immutable(bytes32) + + +# ----------------------- Contract ------------------------------------------- + +@external +def __init__( + _name: String[64], + _symbol: String[32], + _coins: address[N_COINS], + _math: address, + _weth: address, # unused but factory has it. + _salt: bytes32, + __packed_precisions: uint256, + packed_A_gamma: uint256, + packed_fee_params: uint256, + packed_rebalancing_params: uint256, + packed_prices: uint256, +): + MATH = Math(_math) + factory = Factory(msg.sender) + name = _name + symbol = _symbol + coins = _coins + + PRECISIONS = self._unpack_3(__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 + + self.packed_rebalancing_params = packed_rebalancing_params # <-- Contains + # rebalancing params: allowed_extra_profit, adjustment_step, + # and ma_exp_time. + + self.packed_fee_params = packed_fee_params # <-------------- Contains Fee + # params: mid_fee, out_fee and fee_gamma. + + self.price_scale_packed = packed_prices + self.price_oracle_packed = packed_prices + self.last_prices_packed = packed_prices + self.last_timestamp = self._pack_2(block.timestamp, block.timestamp) + self.xcp_profit_a = 10**18 + self.xcp_ma_time = 62324 # <--------- 12 hours default on contract start. + + # Cache DOMAIN_SEPARATOR. If chain.id is not CACHED_CHAIN_ID, then + # DOMAIN_SEPARATOR will be re-calculated each time `permit` is called. + # Otherwise, it will always use CACHED_DOMAIN_SEPARATOR. + # see: `_domain_separator()` for its implementation. + NAME_HASH = keccak256(name) + salt = _salt + CACHED_CHAIN_ID = chain.id + CACHED_DOMAIN_SEPARATOR = keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + + log Transfer(empty(address), self, 0) # <------- Fire empty transfer from + # 0x0 to self for indexers to catch. + + +# ------------------- Token transfers in and out of the AMM ------------------ + + +@internal +def _transfer_in( + _coin_idx: 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. + @params _coin_idx uint256 Index of the coin to transfer in. + @params dx amount of `_coin` to transfer into the pool. + @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) + + if expect_optimistic_transfer: # Only enabled in exchange_received: + # it expects the caller of exchange_received to have sent tokens to + # the pool before calling this method. + + # If someone donates extra tokens to the contract: do not acknowledge. + # We only want to know if there are dx amount of tokens. Anything extra, + # we ignore. This is why we need to check if received_amounts (which + # accounts for coin balances of the contract) is atleast dx. + # If we checked for received_amounts == dx, an extra transfer without a + # call to exchange_received will break the method. + dx: uint256 = coin_balance - self.balances[_coin_idx] + assert dx >= _dx # dev: user didn't give us coins + + # Adjust balances + self.balances[_coin_idx] += dx + + return dx + + # ----------------------------------------------- ERC20 transferFrom flow. + + # EXTERNAL CALL + assert ERC20(coins[_coin_idx]).transferFrom( + sender, + self, + _dx, + default_return_value=True + ) + + dx: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance + self.balances[_coin_idx] += dx + return dx + + +@internal +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`, `_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 + """ + + # Adjust balances before handling transfers: + self.balances[_coin_idx] -= _amount + + # EXTERNAL CALL + assert ERC20(coins[_coin_idx]).transfer( + receiver, + _amount, + default_return_value=True + ) + + +# -------------------------- AMM Main Functions ------------------------------ + + +@external +@nonreentrant("lock") +def exchange( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + receiver: address = msg.sender +) -> uint256: + """ + @notice Exchange using wrapped native token by default + @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 + """ + # _transfer_in updates self.balances here: + dx_received: uint256 = self._transfer_in( + i, + dx, + msg.sender, + False + ) + + # No ERC20 token transfers occur here: + out: uint256[3] = self._exchange( + i, + j, + dx_received, + min_dy, + ) + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(j, out[0], receiver) + + # log: + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) + + return out[0] + + +@external +@nonreentrant('lock') +def exchange_received( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + receiver: address = msg.sender, +) -> uint256: + """ + @notice Exchange: but user must transfer dx amount of coin[i] tokens to pool first. + Pool will not call transferFrom and will only check if a surplus of + coins[i] is greater than or equal to `dx`. + @dev Use-case is to reduce the number of redundant ERC20 token + transfers in zaps. Primarily for dex-aggregators/arbitrageurs/searchers. + Note for users: please transfer + exchange_received in 1 tx. + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param receiver Address to send the output coin to + @return uint256 Amount of tokens at index j received by the `receiver` + """ + # _transfer_in updates self.balances here: + dx_received: uint256 = self._transfer_in( + i, + dx, + msg.sender, + True # <---- expect_optimistic_transfer is set to True here. + ) + + # No ERC20 token transfers occur here: + out: uint256[3] = self._exchange( + i, + j, + dx_received, + min_dy, + ) + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(j, out[0], receiver) + + # log: + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) + + return out[0] + + +@external +@nonreentrant("lock") +def add_liquidity( + amounts: uint256[N_COINS], + min_mint_amount: uint256, + 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 receiver Address to send the LP tokens to. Default is msg.sender + @return uint256 Amount of LP tokens received by the `receiver + """ + + A_gamma: uint256[2] = self._A_gamma() + xp: uint256[N_COINS] = self.balances + amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) + d_token: uint256 = 0 + d_token_fee: uint256 = 0 + old_D: uint256 = 0 + + assert amounts[0] + amounts[1] + amounts[2] > 0 # dev: no coins to add + + # --------------------- Get prices, balances ----------------------------- + + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS-1] = self._unpack_prices(packed_price_scale) + + # -------------------------------------- Update balances and calculate xp. + xp_old: uint256[N_COINS] = xp + amounts_received: uint256[N_COINS] = empty(uint256[N_COINS]) + + ########################## TRANSFER IN <------- + + for i in range(N_COINS): + if amounts[i] > 0: + # Updates self.balances here: + amounts_received[i] = self._transfer_in( + i, + amounts[i], + msg.sender, + False, # <--------------------- Disable optimistic transfers. + ) + xp[i] = xp[i] + amounts_received[i] + + xp[0] *= PRECISIONS[0] + xp_old[0] *= PRECISIONS[0] + for i in range(N_COINS): + + 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 ----------------------- + + if self.future_A_gamma_time > block.timestamp: # <--- A_gamma is ramping. + + # ----- Recalculate the invariant if A or gamma are undergoing a ramp. + old_D = MATH.newton_D(A_gamma[0], A_gamma[1], xp_old, 0) + + else: + + old_D = self.D + + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + + token_supply: uint256 = self.totalSupply + if old_D > 0: + d_token = token_supply * D / old_D - token_supply + else: + d_token = self.get_xcp(D, packed_price_scale) # <----- Making initial + # virtual price equal to 1. + + assert d_token > 0 # dev: nothing minted + + if old_D > 0: + + d_token_fee = ( + self._calc_token_fee(amountsp, xp) * d_token / 10**10 + 1 + ) + + d_token -= d_token_fee + token_supply += d_token + self.mint(receiver, d_token) + self.admin_lp_virtual_balance += unsafe_div(ADMIN_FEE * d_token_fee, 10**10) + + packed_price_scale = self.tweak_price(A_gamma, xp, D, 0) + + else: + + # (re)instatiating an empty pool: + + self.D = D + self.virtual_price = 10**18 + self.xcp_profit = 10**18 + self.xcp_profit_a = 10**18 + + # Initialise xcp oracle here: + self.cached_xcp_oracle = d_token # <--- virtual_price * totalSupply / 10**18 + + self.mint(receiver, d_token) + + assert d_token >= min_mint_amount, "Slippage" + + # ---------------------------------------------- Log and claim admin fees. + + log AddLiquidity( + receiver, amounts_received, d_token_fee, token_supply, packed_price_scale + ) + + return d_token + + +@external +@nonreentrant("lock") +def remove_liquidity( + _amount: uint256, + min_amounts: uint256[N_COINS], + receiver: address = msg.sender, +) -> uint256[N_COINS]: + """ + @notice This withdrawal method is very safe, does no complex math since + tokens are withdrawn in balanced proportions. No fees are charged. + @param _amount Amount of LP tokens to burn + @param min_amounts Minimum amounts of tokens to withdraw + @param receiver Address to send the withdrawn tokens to + @return uint256[3] Amount of pool tokens received by the `receiver` + """ + amount: uint256 = _amount + balances: uint256[N_COINS] = self.balances + withdraw_amounts: uint256[N_COINS] = empty(uint256[N_COINS]) + + # -------------------------------------------------------- Burn LP tokens. + + total_supply: uint256 = self.totalSupply # <------ Get totalSupply before + self.burnFrom(msg.sender, _amount) # ---- reducing it with self.burnFrom. + + # There are two cases for withdrawing tokens from the pool. + # Case 1. Withdrawal does not empty the pool. + # In this situation, D is adjusted proportional to the amount of + # LP tokens burnt. ERC20 tokens transferred is proportional + # to : (AMM balance * LP tokens in) / LP token total supply + # Case 2. Withdrawal empties the pool. + # In this situation, all tokens are withdrawn and the invariant + # is reset. + + if amount == total_supply: # <----------------------------------- Case 2. + + for i in range(N_COINS): + + 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): + + 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 + # proportional to the amount of tokens leaving. Since withdrawals are + # balanced, this is a simple subtraction. If amount == total_supply, + # D will be 0. + + # ---------------------------------- Transfers --------------------------- + + for i in range(N_COINS): + # _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) + + # --------------------------- Upkeep xcp oracle -------------------------- + + # Update xcp since liquidity was removed: + xp: uint256[N_COINS] = self.xp(self.balances, self.price_scale_packed) + last_xcp: uint256 = MATH.geometric_mean(xp) # <----------- Cache it for now. + + last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) + if last_timestamp[1] < block.timestamp: + + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + alpha: uint256 = self._alpha(last_timestamp[1], self.xcp_ma_time) + + self.cached_xcp_oracle = unsafe_div( + last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, + 10**18 + ) + last_timestamp[1] = block.timestamp + + # Pack and store timestamps: + self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) + + # Store last xcp + self.last_xcp = last_xcp + + return withdraw_amounts + + +@external +@nonreentrant("lock") +def remove_liquidity_one_coin( + token_amount: uint256, + i: uint256, + min_amount: uint256, + receiver: address = msg.sender +) -> uint256: + """ + @notice Withdraw liquidity in a single token. + Involves fees (lower than swap fees). + @dev This operation also involves an admin fee claim. + @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 receiver Address to send the withdrawn tokens to + @return Amount of tokens at index i received by the `receiver` + """ + + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. + + A_gamma: uint256[2] = self._A_gamma() + + dy: uint256 = 0 + D: uint256 = 0 + p: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + approx_fee: uint256 = 0 + + # ------------------------------------------------------------------------ + + dy, D, xp, approx_fee = self._calc_withdraw_one_coin( + A_gamma, + token_amount, + i, + (self.future_A_gamma_time > block.timestamp), # <------- During ramps + ) # we need to update D. + + assert dy >= min_amount, "Slippage" + + # ---------------------------- State Updates ----------------------------- + + # Burn user's tokens: + self.burnFrom(msg.sender, token_amount) + + packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0) + # Safe to use D from _calc_withdraw_one_coin here ---^ + + # ------------------------- Transfers ------------------------------------ + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(i, dy, receiver) + + log RemoveLiquidityOne( + msg.sender, token_amount, i, dy, approx_fee, packed_price_scale + ) + + return dy + + +# -------------------------- Packing functions ------------------------------- + + +@internal +@pure +def _pack_3(x: uint256[3]) -> uint256: + """ + @notice Packs 3 integers with values <= 10**18 into a uint256 + @param x The uint256[3] to pack + @return uint256 Integer with packed values + """ + return (x[0] << 128) | (x[1] << 64) | x[2] + + +@internal +@pure +def _unpack_3(_packed: uint256) -> uint256[3]: + """ + @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) + @param val The uint256 to unpack + @return uint256[3] A list of length 3 with unpacked integers + """ + return [ + (_packed >> 128) & 18446744073709551615, + (_packed >> 64) & 18446744073709551615, + _packed & 18446744073709551615, + ] + + + +@pure +@internal +def _pack_2(p1: uint256, p2: uint256) -> uint256: + return p1 | (p2 << 128) + + +@pure +@internal +def _unpack_2(packed: uint256) -> uint256[2]: + return [packed & (2**128 - 1), packed >> 128] + + +@internal +@pure +def _pack_prices(prices_to_pack: uint256[N_COINS-1]) -> uint256: + """ + @notice Packs N_COINS-1 prices into a uint256. + @param prices_to_pack The prices to pack + @return uint256 An integer that packs prices + """ + packed_prices: uint256 = 0 + p: uint256 = 0 + for k in range(N_COINS - 1): + packed_prices = packed_prices << PRICE_SIZE + p = prices_to_pack[N_COINS - 2 - k] + assert p < PRICE_MASK + packed_prices = p | packed_prices + return packed_prices + + +@internal +@pure +def _unpack_prices(_packed_prices: uint256) -> uint256[2]: + """ + @notice Unpacks N_COINS-1 prices from a uint256. + @param _packed_prices The packed prices + @return uint256[2] Unpacked prices + """ + unpacked_prices: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + packed_prices: uint256 = _packed_prices + for k in range(N_COINS - 1): + unpacked_prices[k] = packed_prices & PRICE_MASK + packed_prices = packed_prices >> PRICE_SIZE + + return unpacked_prices + + +# ---------------------- AMM Internal Functions ------------------------------- + + +@internal +def _exchange( + i: uint256, + j: uint256, + dx_received: uint256, + min_dy: uint256, +) -> uint256[3]: + + assert i != j # dev: coin index out of range + assert dx_received > 0 # dev: do not exchange 0 coins + + A_gamma: uint256[2] = self._A_gamma() + xp: uint256[N_COINS] = self.balances # <------- Has dx added to balances. + dy: uint256 = 0 + + y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert. + x0: uint256 = xp[i] - dx_received # old xp[i] + + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS - 1] = self._unpack_prices( + packed_price_scale + ) + + xp[0] *= PRECISIONS[0] + for k in range(1, N_COINS): + xp[k] = unsafe_div( + 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] + + # ----------- Update invariant if A, gamma are undergoing ramps --------- + + t: uint256 = self.future_A_gamma_time + if t > block.timestamp: + + x0 *= prec_i + + if i > 0: + x0 = unsafe_div(x0 * price_scale[i - 1], PRECISION) + + x1: uint256 = xp[i] # <------------------ Back up old value in xp ... + xp[i] = x0 # | + self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # | + xp[i] = x1 # <-------------------------------------- ... and restore. + + # ----------------------- Calculate dy and fees -------------------------- + + D: uint256 = self.D + y_out: uint256[2] = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, j) + dy = xp[j] - y_out[0] + xp[j] -= dy + dy -= 1 + + if j > 0: + dy = dy * PRECISION / price_scale[j - 1] + dy /= PRECISIONS[j] + + fee: uint256 = unsafe_div(self._fee(xp) * dy, 10**10) + dy -= fee # <--------------------- Subtract fee from the outgoing amount. + assert dy >= min_dy, "Slippage" + + y -= dy + + y *= PRECISIONS[j] + if j > 0: + 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]) + + return [dy, fee, packed_price_scale] + + +@internal +def tweak_price( + A_gamma: uint256[2], + _xp: uint256[N_COINS], + new_D: uint256, + K0_prev: uint256 = 0, +) -> uint256: + """ + @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. + @dev Contains main liquidity rebalancing logic, by tweaking `price_scale`. + @param A_gamma Array of A and gamma parameters. + @param _xp Array of current balances. + @param new_D New D value. + @param K0_prev Initial guess for `newton_D`. + """ + + # ---------------------------- Read storage ------------------------------ + + 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) + rebalancing_params: uint256[3] = self._unpack_3(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 + + # ----------------------- Update Oracles if needed ----------------------- + + last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) + alpha: uint256 = 0 + if last_timestamp[0] < block.timestamp: # 0th index is for price_oracle. + + # 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. + + # ------------------ Calculate moving average params ----------------- + + alpha = self._alpha(last_timestamp[0], rebalancing_params[2]) + for k in range(N_COINS - 1): + + # ----------------- We cap state price that goes into the EMA with + # 2 x price_scale. + price_oracle[k] = unsafe_div( + min(last_prices[k], 2 * price_scale[k]) * (10**18 - alpha) + + price_oracle[k] * alpha, # ^-------- Cap spot price into EMA. + 10**18 + ) + + self.price_oracle_packed = self._pack_prices(price_oracle) + last_timestamp[0] = block.timestamp + + # ----------------------------------------------------- Update xcp oracle. + + if last_timestamp[1] < block.timestamp: + + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + alpha = self._alpha(last_timestamp[1], self.xcp_ma_time) + self.cached_xcp_oracle = unsafe_div( + self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, + 10**18 + ) + + # Pack and store timestamps: + last_timestamp[1] = block.timestamp + + self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) + + # `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 ------------------ + + D_unadjusted: uint256 = new_D + if new_D == 0: # <--------------------------- _exchange sets new_D to 0. + D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev) + + # ----------------------- Calculate last_prices -------------------------- + + last_prices = MATH.get_p(_xp, D_unadjusted, A_gamma) + for k in range(N_COINS - 1): + last_prices[k] = unsafe_div(last_prices[k] * price_scale[k], 10**18) + self.last_prices_packed = self._pack_prices(last_prices) + + # ---------- Update profit numbers without price adjustment first -------- + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + xp[0] = unsafe_div(D_unadjusted, N_COINS) + for k in range(N_COINS - 1): + xp[k + 1] = D_unadjusted * 10**18 / (N_COINS * price_scale[k]) + + # ------------------------- Update xcp_profit ---------------------------- + + xcp_profit: uint256 = 10**18 + virtual_price: uint256 = 10**18 + + if old_virtual_price > 0: + + xcp: uint256 = MATH.geometric_mean(xp) + virtual_price = 10**18 * xcp / total_supply + + xcp_profit = unsafe_div( + old_xcp_profit * virtual_price, + old_virtual_price + ) # <---------------- Safu to do unsafe_div as old_virtual_price > 0. + + # If A and gamma are not undergoing ramps (t < block.timestamp), + # ensure new virtual_price is not less than old virtual_price, + # else the pool suffers a loss. + if self.future_A_gamma_time < block.timestamp: + assert virtual_price > old_virtual_price, "Loss" + + # -------------------------- Cache last_xcp -------------------------- + + self.last_xcp = xcp # geometric_mean(D * price_scale) + + self.xcp_profit = xcp_profit + + # ------------ Rebalance liquidity if there's enough profits to adjust it: + if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]: + # allowed_extra_profit --------^ + + # ------------------- Get adjustment step ---------------------------- + + # Calculate the vector distance between price_scale and + # price_oracle. + norm: uint256 = 0 + ratio: uint256 = 0 + for k in range(N_COINS - 1): + + ratio = unsafe_div(price_oracle[k] * 10**18, price_scale[k]) + # unsafe_div because we did safediv before ----^ + + if ratio > 10**18: + ratio = unsafe_sub(ratio, 10**18) + else: + ratio = unsafe_sub(10**18, ratio) + norm = unsafe_add(norm, ratio**2) + + norm = isqrt(norm) # <-------------------- isqrt is not in base 1e18. + adjustment_step: uint256 = max( + rebalancing_params[1], unsafe_div(norm, 5) + ) # ^------------------------------------- adjustment_step. + + if norm > adjustment_step: # <---------- We only adjust prices if the + # vector distance between price_oracle and price_scale is + # large enough. This check ensures that no rebalancing + # occurs if the distance is low i.e. the pool prices are + # pegged to the oracle prices. + + # ------------------------------------- Calculate new price scale. + + p_new: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1]) + for k in range(N_COINS - 1): + p_new[k] = unsafe_div( + price_scale[k] * unsafe_sub(norm, adjustment_step) + + adjustment_step * price_oracle[k], + norm + ) # <- norm is non-zero and gt adjustment_step; unsafe = safe + + # ---------------- Update stale xp (using price_scale) with p_new. + xp = _xp + for k in range(N_COINS - 1): + xp[k + 1] = unsafe_div(_xp[k + 1] * p_new[k], price_scale[k]) + # unsafe_div because we did safediv before ----^ + + # ------------------------------------------ Update D with new xp. + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + assert D > 0 # dev: unsafe D + # Check if calculated p_new is safu: + for k in range(N_COINS): + frac: uint256 = unsafe_div(xp[k] * 10**18, D) + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe p_new + + xp[0] = unsafe_div(D, N_COINS) + for k in range(N_COINS - 1): + xp[k + 1] = D * 10**18 / (N_COINS * p_new[k]) # <---- Convert + # xp to real prices. + + # ---------- Calculate new virtual_price using new xp and D. Reuse + # `old_virtual_price` (but it has new virtual_price). + old_virtual_price = unsafe_div( + 10**18 * MATH.geometric_mean(xp), total_supply + ) # <----- unsafe_div because we did safediv before (if vp>1e18) + + # ---------------------------- Proceed if we've got enough profit. + if ( + old_virtual_price > 10**18 and + 2 * old_virtual_price - 10**18 > xcp_profit + ): + + packed_price_scale = self._pack_prices(p_new) + + self.D = D + self.virtual_price = old_virtual_price + self.price_scale_packed = packed_price_scale + + return packed_price_scale + + # --------- price_scale was not adjusted. Update the profit counter and D. + self.D = D_unadjusted + self.virtual_price = virtual_price + + return packed_price_scale + + +@internal +def _claim_admin_fees(): + """ + @notice Claims admin fees and sends it to fee_receiver set in the factory. + @dev Functionally similar to: + 1. Calculating admin's share of fees, + 2. minting LP tokens, + 3. admin claims underlying tokens via remove_liquidity. + """ + + # --------------------- Check if fees can be claimed --------------------- + + # Disable fee claiming if: + # 1. If time passed since last fee claim is less than + # MIN_ADMIN_FEE_CLAIM_INTERVAL. + # 2. Pool parameters are being ramped. + + last_claim_time: uint256 = self.last_admin_fee_claim_timestamp + if ( + unsafe_sub(block.timestamp, last_claim_time) < MIN_ADMIN_FEE_CLAIM_INTERVAL or + self.future_A_gamma_time > block.timestamp + ): + return + + xcp_profit: uint256 = self.xcp_profit # <---------- Current pool profits. + xcp_profit_a: uint256 = self.xcp_profit_a # <- Profits at previous claim. + current_lp_token_supply: uint256 = self.totalSupply + + # 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 current_lp_token_supply < 10**18: + return + + # ---------- Conditions met to claim admin fees: compute state. ---------- + + A_gamma: uint256[2] = self._A_gamma() + D: uint256 = self.D + vprice: uint256 = self.virtual_price + packed_price_scale: uint256 = self.price_scale_packed + fee_receiver: address = factory.fee_receiver() + balances: uint256[N_COINS] = self.balances + + # Admin fees are calculated as follows. + # 1. Calculate accrued profit since last claim. `xcp_profit` + # is the current profits. `xcp_profit_a` is the profits + # at the previous claim. + # 2. Take out admin's share, which is hardcoded at 5 * 10**9. + # (50% => half of 100% => 10**10 / 2 => 5 * 10**9). + # 3. Since half of the profits go to rebalancing the pool, we + # are left with half; so divide by 2. + + fees: uint256 = unsafe_div( + unsafe_sub(xcp_profit, xcp_profit_a) * ADMIN_FEE, 2 * 10**10 + ) + + # ------------------------------ Claim admin fees by minting admin's share + # of the pool in LP tokens. + + # This is the admin fee tokens claimed in self.add_liquidity. We add it to + # the LP token share that the admin needs to claim: + admin_share: uint256 = self.admin_lp_virtual_balance + frac: uint256 = 0 + if fee_receiver != empty(address) and fees > 0: + + # -------------------------------- Calculate admin share to be minted. + frac = vprice * 10**18 / (vprice - fees) - 10**18 + admin_share += current_lp_token_supply * frac / 10**18 + + # ------ Subtract fees from profits that will be used for rebalancing. + xcp_profit -= fees * 2 + + # ------------------- Recalculate virtual_price following admin fee claim. + total_supply_including_admin_share: uint256 = ( + current_lp_token_supply + admin_share + ) + vprice = ( + 10**18 * self.get_xcp(D, packed_price_scale) / + total_supply_including_admin_share + ) + + # Do not claim fees if doing so causes virtual price to drop below 10**18. + if vprice < 10**18: + return + + # ---------------------------- Update State ------------------------------ + + # Set admin virtual LP balances to zero because we claimed: + self.admin_lp_virtual_balance = 0 + + self.xcp_profit = xcp_profit + self.last_admin_fee_claim_timestamp = block.timestamp + + # 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: + + for i in range(N_COINS): + + admin_tokens[i] = ( + balances[i] * admin_share / + total_supply_including_admin_share + ) + + # _transfer_out tokens to admin and update self.balances. State + # update to self.balances occurs before external contract calls: + self._transfer_out(i, admin_tokens[i], fee_receiver) + + log ClaimAdminFee(fee_receiver, admin_tokens) + + +@internal +@pure +def xp( + balances: uint256[N_COINS], + price_scale_packed: uint256, +) -> uint256[N_COINS]: + + result: 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 + packed_prices = packed_prices >> PRICE_SIZE + + return result + + +@internal +@view +def _alpha(last_timestamp: uint256, ma_exp_time: uint256) -> uint256: + + return MATH.wad_exp( + -convert( + unsafe_div( + (block.timestamp - last_timestamp) * 10**18, + ma_exp_time + ), + int256, + ) + ) + + +@view +@internal +def _A_gamma() -> uint256[2]: + t1: uint256 = self.future_A_gamma_time + + A_gamma_1: uint256 = self.future_A_gamma + gamma1: uint256 = A_gamma_1 & 2**128 - 1 + A1: uint256 = A_gamma_1 >> 128 + + if block.timestamp < t1: + + # --------------- Handle ramping up and down of A -------------------- + + A_gamma_0: uint256 = self.initial_A_gamma + t0: uint256 = self.initial_A_gamma_time + + t1 -= t0 + t0 = block.timestamp - t0 + t2: uint256 = t1 - t0 + + A1 = ((A_gamma_0 >> 128) * t2 + A1 * t0) / t1 + gamma1 = ((A_gamma_0 & 2**128 - 1) * t2 + gamma1 * t0) / t1 + + return [A1, gamma1] + + +@internal +@view +def _fee(xp: uint256[N_COINS]) -> uint256: + + fee_params: uint256[3] = self._unpack_3(self.packed_fee_params) + f: uint256 = MATH.reduction_coefficient(xp, fee_params[2]) + + return unsafe_div( + fee_params[0] * f + fee_params[1] * (10**18 - f), + 10**18 + ) + + +@internal +@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 = price_scale_packed # <------ No precisions here + # because we don't switch to "real" units. + + for i in range(1, N_COINS): + x[i] = D * 10**18 / (N_COINS * (packed_prices & PRICE_MASK)) + packed_prices = packed_prices >> PRICE_SIZE + + return MATH.geometric_mean(x) + + +@view +@internal +def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256: + # fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts) + fee: uint256 = unsafe_div( + unsafe_mul(self._fee(xp), N_COINS), + unsafe_mul(4, unsafe_sub(N_COINS, 1)) + ) + + S: uint256 = 0 + for _x in amounts: + S += _x + + avg: uint256 = unsafe_div(S, N_COINS) + Sdiff: uint256 = 0 + + for _x in amounts: + if _x > avg: + Sdiff += unsafe_sub(_x, avg) + else: + Sdiff += unsafe_sub(avg, _x) + + return fee * Sdiff / S + NOISE_FEE + + +@internal +@view +def _calc_withdraw_one_coin( + A_gamma: uint256[2], + token_amount: uint256, + i: uint256, + update_D: bool, +) -> (uint256, uint256, uint256[N_COINS], uint256): + + token_supply: uint256 = self.totalSupply + assert token_amount <= token_supply # dev: token amount more than supply + assert i < N_COINS # dev: coin out of range + + xx: uint256[N_COINS] = self.balances + xp: uint256[N_COINS] = PRECISIONS + D0: uint256 = 0 + + # -------------------------- Calculate D0 and xp ------------------------- + + 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): + p: uint256 = (packed_prices & PRICE_MASK) + if i == k: + price_scale_i = p * xp[i] + xp[k] = unsafe_div(xp[k] * xx[k] * p, PRECISION) + packed_prices = packed_prices >> PRICE_SIZE + + if update_D: # <-------------- D is updated if pool is undergoing a ramp. + D0 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + else: + D0 = self.D + + D: uint256 = D0 + + # -------------------------------- Fee Calc ------------------------------ + + # Charge fees on D. Roughly calculate xp[i] after withdrawal and use that + # to calculate fee. Precision is not paramount here: we just want a + # behavior where the higher the imbalance caused the more fee the AMM + # charges. + + # xp is adjusted assuming xp[0] ~= xp[1] ~= x[2], which is usually not the + # case. We charge self._fee(xp), where xp is an imprecise adjustment post + # withdrawal in one coin. If the withdraw is too large: charge max fee by + # default. This is because the fee calculation will otherwise underflow. + + xp_imprecise: uint256[N_COINS] = xp + xp_correction: uint256 = xp[i] * N_COINS * token_amount / token_supply + fee: uint256 = self._unpack_3(self.packed_fee_params)[1] # <- self.out_fee. + + if xp_correction < xp_imprecise[i]: + xp_imprecise[i] -= xp_correction + fee = self._fee(xp_imprecise) + + dD: uint256 = unsafe_div(token_amount * D, token_supply) + D_fee: uint256 = fee * dD / (2 * 10**10) + 1 # <------- Actual fee on D. + + # --------- Calculate `approx_fee` (assuming balanced state) in ith token. + # -------------------------------- We only need this for fee in the event. + approx_fee: uint256 = N_COINS * D_fee * xx[i] / D + + # ------------------------------------------------------------------------ + D -= (dD - D_fee) # <----------------------------------- Charge fee on D. + # --------------------------------- Calculate `y_out`` with `(D - D_fee)`. + y: uint256 = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, i)[0] + dy: uint256 = (xp[i] - y) * PRECISION / price_scale_i + xp[i] = y + + return dy, D, xp, approx_fee + + +# ------------------------ ERC20 functions ----------------------------------- + + +@internal +def _approve(_owner: address, _spender: address, _value: uint256): + self.allowance[_owner][_spender] = _value + + log Approval(_owner, _spender, _value) + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + assert _to not in [self, empty(address)] + + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + + log Transfer(_from, _to, _value) + + +@view +@internal +def _domain_separator() -> bytes32: + if chain.id != CACHED_CHAIN_ID: + return keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + return CACHED_DOMAIN_SEPARATOR + + +@external +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + """ + @dev Transfer tokens from one address to another. + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + @return bool True on successul transfer. Reverts otherwise. + """ + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != max_value(uint256): + self._approve(_from, msg.sender, _allowance - _value) + + self._transfer(_from, _to, _value) + return True + + +@external +def transfer(_to: address, _value: uint256) -> bool: + """ + @dev Transfer token for a specified address + @param _to The address to transfer to. + @param _value The amount to be transferred. + @return bool True on successful transfer. Reverts otherwise. + """ + self._transfer(msg.sender, _to, _value) + return True + + +@external +def approve(_spender: address, _value: uint256) -> bool: + """ + @notice Allow `_spender` to transfer up to `_value` amount + of tokens from the caller's account. + @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. + @return bool Success + """ + self._approve(msg.sender, _spender, _value) + return True + + +@external +def permit( + _owner: address, + _spender: address, + _value: uint256, + _deadline: uint256, + _v: uint8, + _r: bytes32, + _s: bytes32, +) -> bool: + """ + @notice Permit `_spender` to spend up to `_value` amount of `_owner`'s + tokens via a signature. + @dev In the event of a chain fork, replay attacks are prevented as + domain separator is recalculated. However, this is only if the + resulting chains update their chainId. + @param _owner The account which generated the signature and is granting an + allowance. + @param _spender The account which will be granted an allowance. + @param _value The approval amount. + @param _deadline The deadline by which the signature must be submitted. + @param _v The last byte of the ECDSA signature. + @param _r The first 32 bytes of the ECDSA signature. + @param _s The second 32 bytes of the ECDSA signature. + @return bool Success. + """ + assert _owner != empty(address) # dev: invalid owner + assert block.timestamp <= _deadline # dev: permit expired + + nonce: uint256 = self.nonces[_owner] + digest: bytes32 = keccak256( + concat( + b"\x19\x01", + self._domain_separator(), + keccak256( + _abi_encode( + EIP2612_TYPEHASH, _owner, _spender, _value, nonce, _deadline + ) + ), + ) + ) + assert ecrecover(digest, _v, _r, _s) == _owner # dev: invalid signature + + self.nonces[_owner] = unsafe_add(nonce, 1) # <-- Unsafe add is safe here. + self._approve(_owner, _spender, _value) + return True + + +@internal +def mint(_to: address, _value: uint256) -> bool: + """ + @dev Mint an amount of the token and assigns it to an account. + This encapsulates the modification of balances such that the + proper events are emitted. + @param _to The account that will receive the created tokens. + @param _value The amount that will be created. + @return bool Success. + """ + self.totalSupply += _value + self.balanceOf[_to] += _value + + log Transfer(empty(address), _to, _value) + return True + + +@internal +def burnFrom(_to: address, _value: uint256) -> bool: + """ + @dev Burn an amount of the token from a given account. + @param _to The account whose tokens will be burned. + @param _value The amount that will be burned. + @return bool Success. + """ + self.totalSupply -= _value + self.balanceOf[_to] -= _value + + log Transfer(_to, empty(address), _value) + return True + + +# ------------------------- AMM View Functions ------------------------------- + + +@external +@view +def fee_receiver() -> address: + """ + @notice Returns the address of the admin fee receiver. + @return address Fee receiver. + """ + return factory.fee_receiver() + + +@external +@view +def admin() -> address: + """ + @notice Returns the address of the pool's admin. + @return address Admin. + """ + return factory.admin() + + +@external +@view +def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256: + """ + @notice Calculate LP tokens minted or to be burned for depositing or + removing `amounts` of coins + @dev Includes fee. + @param amounts Amounts of tokens being deposited or withdrawn + @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.views_implementation() + return Views(view_contract).calc_token_amount(amounts, deposit, self) + + +@external +@view +def get_dy(i: uint256, j: uint256, dx: uint256) -> uint256: + """ + @notice Get amount of coin[j] tokens received for swapping in dx amount of coin[i] + @dev Includes fee. + @param i index of input token. Check pool.coins(i) to get coin address at ith index + @param j index of output token + @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.views_implementation() + return Views(view_contract).get_dy(i, j, dx, self) + + +@external +@view +def get_dx(i: uint256, j: uint256, dy: uint256) -> uint256: + """ + @notice Get amount of coin[i] tokens to input for swapping out dy amount + of coin[j] + @dev This is an approximate method, and returns estimates close to the input + amount. Expensive to call on-chain. + @param i index of input token. Check pool.coins(i) to get coin address at + ith index + @param j index of output token + @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.views_implementation() + return Views(view_contract).get_dx(i, j, dy, self) + + +@external +@view +@nonreentrant("lock") +def lp_price() -> uint256: + """ + @notice Calculates the current price of the LP token w.r.t coin at the + 0th index + @return uint256 LP price. + """ + + 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 + + +@external +@view +@nonreentrant("lock") +def get_virtual_price() -> uint256: + """ + @notice Calculates the current virtual price of the pool LP token. + @dev Not to be confused with `self.virtual_price` which is a cached + virtual price. + @return uint256 Virtual Price. + """ + return ( + 10**18 * self.get_xcp(self.D, self.price_scale_packed) / + self.totalSupply + ) + + +@external +@view +@nonreentrant("lock") +def price_oracle(k: uint256) -> uint256: + """ + @notice Returns the oracle price of the coin at index `k` w.r.t the coin + at index 0. + @dev The oracle is an exponential moving average, with a periodicity + determined by `self.ma_time`. The aggregated prices are cached state + prices (dy/dx) calculated AFTER the latest trade. + State prices that goes into the EMA are capped at 2 x price_scale. + @param k The index of the coin. + @return uint256 Price oracle value of kth coin. + """ + price_oracle: uint256 = self._unpack_prices(self.price_oracle_packed)[k] + price_scale: uint256 = self._unpack_prices(self.price_scale_packed)[k] + last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[0] + + if last_prices_timestamp < block.timestamp: # <------------ Update moving + # average if needed. + + last_prices: uint256 = self._unpack_prices(self.last_prices_packed)[k] + ma_time: uint256 = self._unpack_3(self.packed_rebalancing_params)[2] + alpha: uint256 = self._alpha(last_prices_timestamp, ma_time) + return ( + min(last_prices, 2 * price_scale) * (10**18 - alpha) + + price_oracle * alpha + ) / 10**18 + + return price_oracle + + +@external +@view +@nonreentrant("lock") +def xcp_oracle() -> uint256: + """ + @notice Returns the oracle value for xcp. + @dev The oracle is an exponential moving average, with a periodicity + determined by `self.xcp_ma_time`. + `TVL` is xcp, calculated as either: + 1. virtual_price * total_supply, OR + 2. self.get_xcp(...), OR + 3. MATH.geometric_mean(xp) + @return uint256 Oracle value of xcp. + """ + + last_xcp_timestamp: uint256 = self._unpack_2(self.last_timestamp)[1] + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + + if last_xcp_timestamp < block.timestamp: + + alpha: uint256 = self._alpha(last_xcp_timestamp, self.xcp_ma_time) + return (self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha) / 10**18 + + return cached_xcp_oracle + + +@external +@view +def last_prices(k: uint256) -> uint256: + """ + @notice Returns last price of the coin at index `k` w.r.t the coin + at index 0. + @dev last_prices returns the quote by the AMM for an infinitesimally small swap + after the last trade. It is not equivalent to the last traded price, and + is computed by taking the partial differential of `x` w.r.t `y`. The + derivative is calculated in `get_p` and then multiplied with price_scale + to give last_prices. + @param k The index of the coin. + @return uint256 Last logged price of coin. + """ + return self._unpack_prices(self.last_prices_packed)[k] + + +@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 + at index 0. + @dev Price scale determines the price band around which liquidity is + concentrated. + @param k The index of the coin. + @return uint256 Price scale of coin. + """ + return self._unpack_prices(self.price_scale_packed)[k] + + +@external +@view +def fee() -> uint256: + """ + @notice Returns the fee charged by the pool at current state. + @dev Not to be confused with the fee charged at liquidity action, since + there the fee is calculated on `xp` AFTER liquidity is added or + removed. + @return uint256 fee bps. + """ + return self._fee(self.xp(self.balances, self.price_scale_packed)) + + +@view +@external +def calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256: + """ + @notice Calculates output tokens with fee + @param token_amount LP Token amount to burn + @param i token in which liquidity is withdrawn + @return uint256 Amount of ith tokens received for burning token_amount LP tokens. + """ + + return self._calc_withdraw_one_coin( + self._A_gamma(), + token_amount, + i, + (self.future_A_gamma_time > block.timestamp) + )[0] + + +@external +@view +def calc_token_fee( + amounts: uint256[N_COINS], xp: uint256[N_COINS] +) -> uint256: + """ + @notice Returns the fee charged on the given amounts for add_liquidity. + @param amounts The amounts of coins being added to the pool. + @param xp The current balances of the pool multiplied by coin precisions. + @return uint256 Fee charged. + """ + return self._calc_token_fee(amounts, xp) + + +@view +@external +def A() -> uint256: + """ + @notice Returns the current pool amplification parameter. + @return uint256 A param. + """ + return self._A_gamma()[0] + + +@view +@external +def gamma() -> uint256: + """ + @notice Returns the current pool gamma parameter. + @return uint256 gamma param. + """ + return self._A_gamma()[1] + + +@view +@external +def mid_fee() -> uint256: + """ + @notice Returns the current mid fee + @return uint256 mid_fee value. + """ + return self._unpack_3(self.packed_fee_params)[0] + + +@view +@external +def out_fee() -> uint256: + """ + @notice Returns the current out fee + @return uint256 out_fee value. + """ + return self._unpack_3(self.packed_fee_params)[1] + + +@view +@external +def fee_gamma() -> uint256: + """ + @notice Returns the current fee gamma + @return uint256 fee_gamma value. + """ + return self._unpack_3(self.packed_fee_params)[2] + + +@view +@external +def allowed_extra_profit() -> uint256: + """ + @notice Returns the current allowed extra profit + @return uint256 allowed_extra_profit value. + """ + return self._unpack_3(self.packed_rebalancing_params)[0] + + +@view +@external +def adjustment_step() -> uint256: + """ + @notice Returns the current adjustment step + @return uint256 adjustment_step value. + """ + return self._unpack_3(self.packed_rebalancing_params)[1] + + +@view +@external +def ma_time() -> uint256: + """ + @notice Returns the current moving average time in seconds + @dev To get time in seconds, the parameter is multipled by ln(2) + One can expect off-by-one errors here. + @return uint256 ma_time value. + """ + return self._unpack_3(self.packed_rebalancing_params)[2] * 694 / 1000 + + +@view +@external +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 PRECISIONS + + +@external +@view +def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract. + """ + @notice Returns the fee charged by the pool at current state. + @param xp The current balances of the pool multiplied by coin precisions. + @return uint256 Fee value. + """ + return self._fee(xp) + + +@view +@external +def DOMAIN_SEPARATOR() -> bytes32: + """ + @notice EIP712 domain separator. + @return bytes32 Domain Separator set for the current chain. + """ + return self._domain_separator() + + +# ------------------------- AMM Admin Functions ------------------------------ + + +@external +def ramp_A_gamma( + future_A: uint256, future_gamma: uint256, future_time: uint256 +): + """ + @notice Initialise Ramping A and gamma parameter values linearly. + @dev Only accessible by factory admin, and only + @param future_A The future A value. + @param future_gamma The future gamma value. + @param future_time The timestamp at which the ramping will end. + """ + assert msg.sender == factory.admin() # dev: only owner + assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing + assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time + + A_gamma: uint256[2] = self._A_gamma() + initial_A_gamma: uint256 = A_gamma[0] << 128 + initial_A_gamma = initial_A_gamma | A_gamma[1] + + assert future_A > MIN_A - 1 + assert future_A < MAX_A + 1 + assert future_gamma > MIN_GAMMA - 1 + assert future_gamma < MAX_GAMMA + 1 + + ratio: uint256 = 10**18 * future_A / A_gamma[0] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + ratio = 10**18 * future_gamma / A_gamma[1] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + self.initial_A_gamma = initial_A_gamma + self.initial_A_gamma_time = block.timestamp + + future_A_gamma: uint256 = future_A << 128 + future_A_gamma = future_A_gamma | future_gamma + self.future_A_gamma_time = future_time + self.future_A_gamma = future_A_gamma + + log RampAgamma( + A_gamma[0], + future_A, + A_gamma[1], + future_gamma, + block.timestamp, + future_time, + ) + + +@external +def stop_ramp_A_gamma(): + """ + @notice Stop Ramping A and gamma parameters immediately. + @dev Only accessible by factory admin. + """ + assert msg.sender == factory.admin() # dev: only owner + + A_gamma: uint256[2] = self._A_gamma() + current_A_gamma: uint256 = A_gamma[0] << 128 + current_A_gamma = current_A_gamma | A_gamma[1] + self.initial_A_gamma = current_A_gamma + self.future_A_gamma = current_A_gamma + self.initial_A_gamma_time = block.timestamp + self.future_A_gamma_time = block.timestamp + + # ------ Now (block.timestamp < t1) is always False, so we return saved A. + + log StopRampA(A_gamma[0], A_gamma[1], block.timestamp) + + +@external +@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. + @dev Only accessible by factory admin. + @param _new_mid_fee The new mid fee. + @param _new_out_fee The new out fee. + @param _new_fee_gamma The new fee gamma. + @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 + + # ----------------------------- Set fee params --------------------------- + + new_mid_fee: uint256 = _new_mid_fee + new_out_fee: uint256 = _new_out_fee + new_fee_gamma: uint256 = _new_fee_gamma + + current_fee_params: uint256[3] = self._unpack_3(self.packed_fee_params) + + if new_out_fee < MAX_FEE + 1: + assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range + else: + new_out_fee = current_fee_params[1] + + if new_mid_fee > MAX_FEE: + new_mid_fee = current_fee_params[0] + assert new_mid_fee <= new_out_fee # dev: mid-fee is too high + + if new_fee_gamma < 10**18: + assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18] + else: + new_fee_gamma = current_fee_params[2] + + self.packed_fee_params = self._pack_3([new_mid_fee, new_out_fee, new_fee_gamma]) + + # ----------------- Set liquidity rebalancing parameters ----------------- + + new_allowed_extra_profit: uint256 = _new_allowed_extra_profit + new_adjustment_step: uint256 = _new_adjustment_step + new_ma_time: uint256 = _new_ma_time + + current_rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params) + + if new_allowed_extra_profit > 10**18: + new_allowed_extra_profit = current_rebalancing_params[0] + + if new_adjustment_step > 10**18: + new_adjustment_step = current_rebalancing_params[1] + + if new_ma_time < 872542: # <----- Calculated as: 7 * 24 * 60 * 60 / ln(2) + assert new_ma_time > 86 # dev: MA time should be longer than 60/ln(2) + else: + new_ma_time = current_rebalancing_params[2] + + self.packed_rebalancing_params = self._pack_3( + [new_allowed_extra_profit, new_adjustment_step, new_ma_time] + ) + + # Set xcp oracle moving average window time: + new_xcp_ma_time: uint256 = _new_xcp_ma_time + if new_xcp_ma_time < 872542: + assert new_xcp_ma_time > 86 # dev: xcp MA time should be longer than 60/ln(2) + else: + new_xcp_ma_time = self.xcp_ma_time + self.xcp_ma_time = new_xcp_ma_time + + # ---------------------------------- LOG --------------------------------- + + log NewParameters( + new_mid_fee, + new_out_fee, + new_fee_gamma, + new_allowed_extra_profit, + new_adjustment_step, + new_ma_time, + _new_xcp_ma_time, + ) diff --git a/contracts/zksync/CurveTricryptoOptimizedWETH.vy b/contracts/zksync/CurveTricryptoOptimizedWETH.vy new file mode 100644 index 0000000..f848e8b --- /dev/null +++ b/contracts/zksync/CurveTricryptoOptimizedWETH.vy @@ -0,0 +1,2157 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@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). +@dev All prices in the AMM are with respect to the first token in the pool. + Supports native token transfers. +""" + +from vyper.interfaces import ERC20 +implements: ERC20 # <--------------------- AMM contract is also the LP token. + +# --------------------------------- Interfaces ------------------------------- + +interface Math: + def geometric_mean(_x: uint256[N_COINS]) -> uint256: view + def wad_exp(_power: int256) -> uint256: view + def cbrt(x: uint256) -> uint256: view + def reduction_coefficient( + x: uint256[N_COINS], fee_gamma: uint256 + ) -> uint256: view + def newton_D( + ANN: uint256, + gamma: uint256, + x_unsorted: uint256[N_COINS], + K0_prev: uint256 + ) -> uint256: view + def get_y( + ANN: uint256, + gamma: uint256, + x: uint256[N_COINS], + D: uint256, + i: uint256, + ) -> uint256[2]: view + def get_p( + _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 + def views_implementation() -> address: view + +interface Views: + def calc_token_amount( + amounts: uint256[N_COINS], deposit: bool, swap: address + ) -> uint256: view + def get_dy( + i: uint256, j: uint256, dx: uint256, swap: address + ) -> uint256: view + def get_dx( + i: uint256, j: uint256, dy: uint256, swap: address + ) -> uint256: view + + +# ------------------------------- Events ------------------------------------- + +event Transfer: + sender: indexed(address) + receiver: indexed(address) + value: uint256 + +event Approval: + owner: indexed(address) + spender: indexed(address) + value: uint256 + +event TokenExchange: + buyer: indexed(address) + sold_id: uint256 + tokens_sold: uint256 + bought_id: uint256 + tokens_bought: uint256 + fee: uint256 + packed_price_scale: uint256 + +event AddLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fee: uint256 + token_supply: uint256 + packed_price_scale: uint256 + +event RemoveLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + token_supply: uint256 + +event RemoveLiquidityOne: + provider: indexed(address) + token_amount: uint256 + coin_index: uint256 + coin_amount: uint256 + 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 + fee_gamma: uint256 + allowed_extra_profit: uint256 + adjustment_step: uint256 + ma_time: uint256 + +event RampAgamma: + initial_A: uint256 + future_A: uint256 + initial_gamma: uint256 + future_gamma: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + current_A: uint256 + current_gamma: uint256 + time: uint256 + +event ClaimAdminFee: + admin: indexed(address) + tokens: uint256 + + +# ----------------------- Storage/State Variables ---------------------------- + +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 + +MATH: public(immutable(Math)) +coins: public(immutable(address[N_COINS])) +factory: public(address) + +price_scale_packed: uint256 # <------------------------ Internal price scale. +price_oracle_packed: uint256 # <------- Price target given by moving average. + +last_prices_packed: uint256 +last_prices_timestamp: public(uint256) + +initial_A_gamma: public(uint256) +initial_A_gamma_time: public(uint256) + +future_A_gamma: public(uint256) +future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. +# This value is 0 (default) when pool is first deployed, and only gets +# populated by block.timestamp + future_time in `ramp_A_gamma` when the +# ramping process is initiated. After ramping is finished +# (i.e. self.future_A_gamma_time < block.timestamp), the variable is left +# and not set to 0. + +balances: public(uint256[N_COINS]) +D: public(uint256) +xcp_profit: public(uint256) +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 ------------- + +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 -------------------- + +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. +MAX_FEE: constant(uint256) = 10 * 10**9 +NOISE_FEE: constant(uint256) = 10**5 # <---------------------------- 0.1 BPS. + +# ----------------------- Admin params --------------------------------------- + +admin_actions_deadline: public(uint256) + +ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 +MIN_RAMP_TIME: 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 +MAX_A_CHANGE: constant(uint256) = 10 +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA: constant(uint256) = 5 * 10**16 + +PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1) +PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1 + +# ----------------------- ERC20 Specific vars -------------------------------- + +name: public(immutable(String[64])) +symbol: public(immutable(String[32])) +decimals: public(constant(uint8)) = 18 +version: public(constant(String[8])) = "v2.0.0" + +balanceOf: public(HashMap[address, uint256]) +allowance: public(HashMap[address, HashMap[address, uint256]]) +totalSupply: public(uint256) +nonces: public(HashMap[address, uint256]) + +EIP712_TYPEHASH: constant(bytes32) = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" +) +EIP2612_TYPEHASH: constant(bytes32) = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +) +VERSION_HASH: constant(bytes32) = keccak256(version) +NAME_HASH: immutable(bytes32) +CACHED_CHAIN_ID: immutable(uint256) +salt: public(immutable(bytes32)) +CACHED_DOMAIN_SEPARATOR: immutable(bytes32) + + +# ----------------------- Contract ------------------------------------------- + +@external +def __init__( + _name: String[64], + _symbol: String[32], + _coins: address[N_COINS], + _math: address, + _weth: address, + _salt: bytes32, + packed_precisions: uint256, + packed_A_gamma: uint256, + packed_fee_params: uint256, + packed_rebalancing_params: uint256, + packed_prices: uint256, +): + + WETH20 = _weth + MATH = Math(_math) + + self.factory = msg.sender + + name = _name + symbol = _symbol + coins = _coins + + self.packed_precisions = 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 + + self.packed_rebalancing_params = packed_rebalancing_params # <-- Contains + # rebalancing params: allowed_extra_profit, adjustment_step, + # and ma_exp_time. + + self.packed_fee_params = packed_fee_params # <-------------- Contains Fee + # params: mid_fee, out_fee and fee_gamma. + + self.price_scale_packed = packed_prices + self.price_oracle_packed = packed_prices + self.last_prices_packed = packed_prices + self.last_prices_timestamp = block.timestamp + self.xcp_profit_a = 10**18 + + # Cache DOMAIN_SEPARATOR. If chain.id is not CACHED_CHAIN_ID, then + # DOMAIN_SEPARATOR will be re-calculated each time `permit` is called. + # Otherwise, it will always use CACHED_DOMAIN_SEPARATOR. + # see: `_domain_separator()` for its implementation. + NAME_HASH = keccak256(name) + salt = _salt + CACHED_CHAIN_ID = chain.id + CACHED_DOMAIN_SEPARATOR = keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + + log Transfer(empty(address), self, 0) # <------- Fire empty transfer from + # 0x0 to self for indexers to catch. + + +# ------------------- 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` + 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 dx amount of `_coin` to transfer into the pool. + @params dy amount of `_coin` to transfer out of the pool. + @params mvalue msg.value if the transfer is ETH, 0 otherwise. + @params callbacker address to call `callback_sig` on. + @params callback_sig signature of the callback function. + @params sender address to transfer `_coin` from. + @params receiver address to transfer `_coin` to. + @params use_eth True if the transfer is ETH, False otherwise. + """ + + if use_eth and _coin == WETH20: + assert mvalue == dx # dev: incorrect eth amount + else: + assert mvalue == 0 # dev: nonzero eth amount + + if callback_sig == empty(bytes32): + + assert ERC20(_coin).transferFrom( + sender, self, dx, default_return_value=True + ) + + else: + + # --------- This part of the _transfer_in logic is only accessible + # by _exchange. + + # First call callback logic and then check if pool + # gets dx amounts of _coins[i], revert otherwise. + b: uint256 = ERC20(_coin).balanceOf(self) + raw_call( + callbacker, + concat( + slice(callback_sig, 0, 4), + _abi_encode(sender, receiver, _coin, dx, dy) + ) + ) + assert ERC20(_coin).balanceOf(self) - b == dx # dev: callback didn't give us coins + # ^------ note: dx cannot + # be 0, so the contract MUST receive some _coin. + + if _coin == WETH20: + WETH(WETH20).withdraw(dx) # <--------- if WETH was transferred in + # previous step and `not use_eth`, withdraw WETH to ETH. + + +@internal +def _transfer_out( + _coin: address, _amount: uint256, use_eth: bool, 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 + @params _amount Amount of token to transfer out + @params use_eth Whether to transfer ETH or not + @params receiver Address to send the tokens to + """ + + if use_eth and _coin == WETH20: + raw_call(receiver, b"", value=_amount) + else: + if _coin == WETH20: + WETH(WETH20).deposit(value=_amount) + + assert ERC20(_coin).transfer( + receiver, _amount, default_return_value=True + ) + + +# -------------------------- AMM Main Functions ------------------------------ + + +@payable +@external +@nonreentrant("lock") +def exchange( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + use_eth: bool = False, + receiver: address = msg.sender +) -> uint256: + """ + @notice Exchange using wrapped native token by default + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param use_eth True if the input coin is native token, False otherwise + @param receiver Address to send the output coin to. Default is msg.sender + @return uint256 Amount of tokens at index j received by the `receiver + """ + return self._exchange( + msg.sender, + msg.value, + i, + j, + dx, + min_dy, + use_eth, + receiver, + empty(address), + empty(bytes32) + ) + + +@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) + ) + + +@external +@nonreentrant('lock') +def exchange_extended( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + use_eth: bool, + sender: address, + receiver: address, + cb: bytes32 +) -> uint256: + """ + @notice Exchange with callback method. + @dev This method does not allow swapping in native token, but does allow + swaps that transfer out native token from the pool. + @dev Does not allow flashloans + @dev One use-case is to reduce the number of redundant ERC20 token + transfers in zaps. + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param use_eth True if output is native token, False otherwise + @param sender Address to transfer input coin from + @param receiver Address to send the output coin to + @param cb Callback signature + @return uint256 Amount of tokens at index j received by the `receiver` + """ + + assert cb != empty(bytes32) # dev: No callback specified + return self._exchange( + sender, 0, i, j, dx, min_dy, use_eth, receiver, msg.sender, cb + ) # callbacker should never be self ------------------^ + + +@payable +@external +@nonreentrant("lock") +def add_liquidity( + amounts: uint256[N_COINS], + min_mint_amount: uint256, + use_eth: bool = False, + receiver: address = msg.sender +) -> uint256: + """ + @notice Adds liquidity into the pool. + @param amounts Amounts of each coin to add. + @param min_mint_amount Minimum amount of LP to mint. + @param use_eth True if native token is being added to the pool. + @param receiver Address to send the LP tokens to. Default is msg.sender + @return uint256 Amount of LP tokens received by the `receiver + """ + + 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 + + assert amounts[0] + amounts[1] + amounts[2] > 0 # dev: no coins to add + + # --------------------- Get prices, balances ----------------------------- + + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS-1] = self._unpack_prices(packed_price_scale) + + # -------------------------------------- Update balances and calculate xp. + xp_old: uint256[N_COINS] = xp + for i in range(N_COINS): + bal: uint256 = xp[i] + amounts[i] + xp[i] = bal + self.balances[i] = bal + xx = xp + + xp[0] *= precisions[0] + xp_old[0] *= precisions[0] + for i in range(1, N_COINS): + xp[i] = unsafe_div(xp[i] * price_scale[i-1] * precisions[i], PRECISION) + xp_old[i] = unsafe_div( + xp_old[i] * unsafe_mul(price_scale[i-1], precisions[i]), + PRECISION + ) + + # ---------------- transferFrom token into the pool ---------------------- + + for i in range(N_COINS): + + 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. + ) + + amountsp[i] = xp[i] - xp_old[i] + + # -------------------- Calculate LP tokens to mint ----------------------- + + if self.future_A_gamma_time > block.timestamp: # <--- A_gamma is ramping. + + # ----- Recalculate the invariant if A or gamma are undergoing a ramp. + old_D = MATH.newton_D(A_gamma[0], A_gamma[1], xp_old, 0) + + else: + + old_D = self.D + + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + + token_supply: uint256 = self.totalSupply + if old_D > 0: + d_token = token_supply * D / old_D - token_supply + else: + d_token = self.get_xcp(D) # <------------------------- Making initial + # virtual price equal to 1. + + assert d_token > 0 # dev: nothing minted + + if old_D > 0: + + d_token_fee = ( + self._calc_token_fee(amountsp, xp) * d_token / 10**10 + 1 + ) + + d_token -= d_token_fee + token_supply += d_token + self.mint(receiver, d_token) + + packed_price_scale = self.tweak_price(A_gamma, xp, D, 0) + + else: + + self.D = D + self.virtual_price = 10**18 + self.xcp_profit = 10**18 + self.xcp_profit_a = 10**18 + self.mint(receiver, d_token) + + assert d_token >= min_mint_amount, "Slippage" + + log AddLiquidity( + receiver, amounts, d_token_fee, token_supply, packed_price_scale + ) + + self._claim_admin_fees() # <--------------------------- Claim admin fees. + + return d_token + + +@external +@nonreentrant("lock") +def remove_liquidity( + _amount: uint256, + min_amounts: uint256[N_COINS], + use_eth: bool = False, + receiver: address = msg.sender, + claim_admin_fees: bool = True, +) -> uint256[N_COINS]: + """ + @notice This withdrawal method is very safe, does no complex math since + tokens are withdrawn in balanced proportions. No fees are charged. + @param _amount Amount of LP tokens to burn + @param min_amounts Minimum amounts of tokens to withdraw + @param use_eth Whether to withdraw ETH or not + @param receiver Address to send the withdrawn tokens to + @param claim_admin_fees If True, call self._claim_admin_fees(). Default is True. + @return uint256[3] Amount of pool tokens received by the `receiver` + """ + amount: uint256 = _amount + balances: uint256[N_COINS] = self.balances + d_balances: uint256[N_COINS] = empty(uint256[N_COINS]) + + if claim_admin_fees: + self._claim_admin_fees() # <------ We claim fees so that the DAO gets + # paid before withdrawal. In emergency cases, set it to False. + + # -------------------------------------------------------- Burn LP tokens. + + total_supply: uint256 = self.totalSupply # <------ Get totalSupply before + self.burnFrom(msg.sender, _amount) # ---- reducing it with self.burnFrom. + + # There are two cases for withdrawing tokens from the pool. + # Case 1. Withdrawal does not empty the pool. + # In this situation, D is adjusted proportional to the amount of + # LP tokens burnt. ERC20 tokens transferred is proportional + # to : (AMM balance * LP tokens in) / LP token total supply + # Case 2. Withdrawal empties the pool. + # In this situation, all tokens are withdrawn and the invariant + # is reset. + + if amount == total_supply: # <----------------------------------- Case 2. + + for i in range(N_COINS): + + d_balances[i] = balances[i] + self.balances[i] = 0 # <------------------------- Empty the pool. + + 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. + + D: uint256 = self.D + self.D = D - unsafe_div(D * amount, total_supply) # <----------- Reduce D + # proportional to the amount of tokens leaving. Since withdrawals are + # balanced, this is a simple subtraction. If amount == total_supply, + # D will be 0. + + # ---------------------------------- Transfers --------------------------- + + for i in range(N_COINS): + self._transfer_out(coins[i], d_balances[i], use_eth, receiver) + + log RemoveLiquidity(msg.sender, balances, total_supply - _amount) + + return d_balances + + +@external +@nonreentrant("lock") +def remove_liquidity_one_coin( + token_amount: uint256, + i: uint256, + min_amount: uint256, + use_eth: bool = False, + receiver: address = msg.sender +) -> uint256: + """ + @notice Withdraw liquidity in a single token. + Involves fees (lower than swap fees). + @dev This operation also involves an admin fee claim. + @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` + """ + + A_gamma: uint256[2] = self._A_gamma() + + dy: uint256 = 0 + D: uint256 = 0 + p: uint256 = 0 + 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( + A_gamma, + token_amount, + i, + (self.future_A_gamma_time > block.timestamp), # <------- During ramps + ) # we need to update D. + + assert dy >= min_amount, "Slippage" + + # ------------------------- Transfers ------------------------------------ + + self.balances[i] -= dy + self.burnFrom(msg.sender, token_amount) + self._transfer_out(coins[i], dy, use_eth, receiver) + + packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0) + # Safe to use D from _calc_withdraw_one_coin here ---^ + + log RemoveLiquidityOne( + msg.sender, token_amount, i, dy, approx_fee, packed_price_scale + ) + + return dy + + +@external +@nonreentrant("lock") +def claim_admin_fees(): + """ + @notice Claim admin fees. Callable by anyone. + """ + self._claim_admin_fees() + + +# -------------------------- Packing functions ------------------------------- + + +@internal +@view +def _pack(x: uint256[3]) -> uint256: + """ + @notice Packs 3 integers with values <= 10**18 into a uint256 + @param x The uint256[3] to pack + @return uint256 Integer with packed values + """ + return (x[0] << 128) | (x[1] << 64) | x[2] + + +@internal +@view +def _unpack(_packed: uint256) -> uint256[3]: + """ + @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) + @param val The uint256 to unpack + @return uint256[3] A list of length 3 with unpacked integers + """ + return [ + (_packed >> 128) & 18446744073709551615, + (_packed >> 64) & 18446744073709551615, + _packed & 18446744073709551615, + ] + + +@internal +@view +def _pack_prices(prices_to_pack: uint256[N_COINS-1]) -> uint256: + """ + @notice Packs N_COINS-1 prices into a uint256. + @param prices_to_pack The prices to pack + @return uint256 An integer that packs prices + """ + packed_prices: uint256 = 0 + p: uint256 = 0 + for k in range(N_COINS - 1): + packed_prices = packed_prices << PRICE_SIZE + p = prices_to_pack[N_COINS - 2 - k] + assert p < PRICE_MASK + packed_prices = p | packed_prices + return packed_prices + + +@internal +@view +def _unpack_prices(_packed_prices: uint256) -> uint256[2]: + """ + @notice Unpacks N_COINS-1 prices from a uint256. + @param _packed_prices The packed prices + @return uint256[2] Unpacked prices + """ + unpacked_prices: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + packed_prices: uint256 = _packed_prices + for k in range(N_COINS - 1): + unpacked_prices[k] = packed_prices & PRICE_MASK + packed_prices = packed_prices >> PRICE_SIZE + + return unpacked_prices + + +# ---------------------- AMM Internal Functions ------------------------------- + + +@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 +) -> uint256: + + assert i != j # dev: coin index out of range + assert dx > 0 # dev: do not exchange 0 coins + + A_gamma: uint256[2] = self._A_gamma() + xp: uint256[N_COINS] = self.balances + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + dy: uint256 = 0 + + y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert. + x0: uint256 = xp[i] # <--------------- if i > N_COINS, this will revert. + xp[i] = x0 + dx + self.balances[i] = xp[i] + + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS - 1] = self._unpack_prices( + packed_price_scale + ) + + xp[0] *= precisions[0] + for k in range(1, N_COINS): + xp[k] = unsafe_div( + 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] + + # ----------- Update invariant if A, gamma are undergoing ramps --------- + + t: uint256 = self.future_A_gamma_time + if t > block.timestamp: + + x0 *= prec_i + + if i > 0: + x0 = unsafe_div(x0 * price_scale[i - 1], PRECISION) + + x1: uint256 = xp[i] # <------------------ Back up old value in xp ... + xp[i] = x0 # | + self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # | + xp[i] = x1 # <-------------------------------------- ... and restore. + + # ----------------------- Calculate dy and fees -------------------------- + + D: uint256 = self.D + 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 + dy -= 1 + + if j > 0: + dy = dy * PRECISION / price_scale[j - 1] + dy /= prec_j + + fee: uint256 = unsafe_div(self._fee(xp) * dy, 10**10) + + dy -= fee # <--------------------- Subtract fee from the outgoing amount. + assert dy >= min_dy, "Slippage" + + y -= dy + self.balances[j] = y # <----------- Update pool balance of outgoing coin. + + y *= prec_j + if j > 0: + y = unsafe_div(y * price_scale[j - 1], PRECISION) + xp[j] = y # <------------------------------------------------- Update xp. + + # ---------------------- Do Transfers in and out ------------------------- + + ########################## TRANSFER IN <------- + self._transfer_in( + coins[i], dx, dy, mvalue, + callbacker, callback_sig, # <-------- Callback method is called here. + sender, receiver, use_eth, + ) + + ########################## -------> TRANSFER OUT + self._transfer_out(coins[j], dy, use_eth, 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 + + +@internal +def tweak_price( + A_gamma: uint256[2], + _xp: uint256[N_COINS], + new_D: uint256, + K0_prev: uint256 = 0, +) -> uint256: + """ + @notice Tweaks 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. + @dev Contains main liquidity rebalancing logic, by tweaking `price_scale`. + @param A_gamma Array of A and gamma parameters. + @param _xp Array of current balances. + @param new_D New D value. + @param K0_prev Initial guess for `newton_D`. + """ + + # ---------------------------- 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 + ) + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS - 1] = self._unpack_prices( + packed_price_scale + ) + + total_supply: uint256 = self.totalSupply + old_xcp_profit: uint256 = self.xcp_profit + old_virtual_price: uint256 = self.virtual_price + last_prices_timestamp: uint256 = self.last_prices_timestamp + + # ----------------------- Update MA if needed ---------------------------- + + if last_prices_timestamp < block.timestamp: + + # 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. + + # ------------------ Calculate moving average params ----------------- + + alpha: uint256 = MATH.wad_exp( + -convert( + unsafe_div( + (block.timestamp - last_prices_timestamp) * 10**18, + rebalancing_params[2] # <----------------------- ma_time. + ), + int256, + ) + ) + + for k in range(N_COINS - 1): + + # ----------------- We cap state price that goes into the EMA with + # 2 x price_scale. + price_oracle[k] = unsafe_div( + min(last_prices[k], 2 * price_scale[k]) * (10**18 - alpha) + + price_oracle[k] * alpha, # ^-------- Cap spot price into EMA. + 10**18 + ) + + 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. + + # ------------------ If new_D is set to 0, calculate it ------------------ + + D_unadjusted: uint256 = new_D + if new_D == 0: # <--------------------------- _exchange sets new_D to 0. + D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev) + + # ----------------------- Calculate last_prices -------------------------- + + last_prices = MATH.get_p(_xp, D_unadjusted, A_gamma) + for k in range(N_COINS - 1): + last_prices[k] = unsafe_div(last_prices[k] * price_scale[k], 10**18) + self.last_prices_packed = self._pack_prices(last_prices) + + # ---------- Update profit numbers without price adjustment first -------- + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + xp[0] = unsafe_div(D_unadjusted, N_COINS) + for k in range(N_COINS - 1): + xp[k + 1] = D_unadjusted * 10**18 / (N_COINS * price_scale[k]) + + # ------------------------- Update xcp_profit ---------------------------- + + xcp_profit: uint256 = 10**18 + virtual_price: uint256 = 10**18 + + if old_virtual_price > 0: + + xcp: uint256 = MATH.geometric_mean(xp) + virtual_price = 10**18 * xcp / total_supply + + xcp_profit = unsafe_div( + old_xcp_profit * virtual_price, + old_virtual_price + ) # <---------------- Safu to do unsafe_div as old_virtual_price > 0. + + # If A and gamma are not undergoing ramps (t < block.timestamp), + # ensure new virtual_price is not less than old virtual_price, + # else the pool suffers a loss. + if self.future_A_gamma_time < block.timestamp: + assert virtual_price > old_virtual_price, "Loss" + + self.xcp_profit = xcp_profit + + # ------------ Rebalance liquidity if there's enough profits to adjust it: + if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]: + # allowed_extra_profit --------^ + + # ------------------- Get adjustment step ---------------------------- + + # Calculate the vector distance between price_scale and + # price_oracle. + norm: uint256 = 0 + ratio: uint256 = 0 + for k in range(N_COINS - 1): + + ratio = unsafe_div(price_oracle[k] * 10**18, price_scale[k]) + # unsafe_div because we did safediv before ----^ + + if ratio > 10**18: + ratio = unsafe_sub(ratio, 10**18) + else: + ratio = unsafe_sub(10**18, ratio) + norm = unsafe_add(norm, ratio**2) + + norm = isqrt(norm) # <-------------------- isqrt is not in base 1e18. + adjustment_step: uint256 = max( + rebalancing_params[1], unsafe_div(norm, 5) + ) # ^------------------------------------- adjustment_step. + + if norm > adjustment_step: # <---------- We only adjust prices if the + # vector distance between price_oracle and price_scale is + # large enough. This check ensures that no rebalancing + # occurs if the distance is low i.e. the pool prices are + # pegged to the oracle prices. + + # ------------------------------------- Calculate new price scale. + + p_new: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1]) + for k in range(N_COINS - 1): + p_new[k] = unsafe_div( + price_scale[k] * unsafe_sub(norm, adjustment_step) + + adjustment_step * price_oracle[k], + norm + ) # <- norm is non-zero and gt adjustment_step; unsafe = safe + + # ---------------- Update stale xp (using price_scale) with p_new. + xp = _xp + for k in range(N_COINS - 1): + xp[k + 1] = unsafe_div(_xp[k + 1] * p_new[k], price_scale[k]) + # unsafe_div because we did safediv before ----^ + + # ------------------------------------------ Update D with new xp. + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + + for k in range(N_COINS): + frac: uint256 = xp[k] * 10**18 / D # <----- Check validity of + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # p_new. + + xp[0] = D / N_COINS + for k in range(N_COINS - 1): + xp[k + 1] = D * 10**18 / (N_COINS * p_new[k]) # <---- Convert + # xp to real prices. + + # ---------- Calculate new virtual_price using new xp and D. Reuse + # `old_virtual_price` (but it has new virtual_price). + old_virtual_price = unsafe_div( + 10**18 * MATH.geometric_mean(xp), total_supply + ) # <----- unsafe_div because we did safediv before (if vp>1e18) + + # ---------------------------- Proceed if we've got enough profit. + if ( + old_virtual_price > 10**18 and + 2 * old_virtual_price - 10**18 > xcp_profit + ): + + packed_price_scale = self._pack_prices(p_new) + + self.D = D + self.virtual_price = old_virtual_price + self.price_scale_packed = packed_price_scale + + return packed_price_scale + + # --------- price_scale was not adjusted. Update the profit counter and D. + self.D = D_unadjusted + self.virtual_price = virtual_price + + return packed_price_scale + + +@internal +def _claim_admin_fees(): + """ + @notice Claims admin fees and sends it to fee_receiver set in the factory. + """ + 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 + + # Do not claim admin fees if: + # 1. insufficient profits accrued since last claim, and + # 2. there are less than 10**18 (or 1 unit of) lp tokens, else it can lead + # to manipulated virtual prices. + if xcp_profit <= xcp_profit_a or total_supply < 10**18: + return + + # Claim tokens belonging to the admin here. This is done by 'gulping' + # pool tokens that have accrued as fees, but not accounted in pool's + # `self.balances` yet: pool balances only account for incoming and + # outgoing tokens excluding fees. Following 'gulps' fees: + + for i in range(N_COINS): + if coins[i] == WETH20: + self.balances[i] = self.balance + else: + self.balances[i] = ERC20(coins[i]).balanceOf(self) + + # If 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 + # at the previous claim. + # 2. Take out admin's share, which is hardcoded at 5 * 10**9. + # (50% => half of 100% => 10**10 / 2 => 5 * 10**9). + # 3. Since half of the profits go to rebalancing the pool, we + # are left with half; so divide by 2. + + fees: uint256 = unsafe_div( + unsafe_sub(xcp_profit, xcp_profit_a) * ADMIN_FEE, 2 * 10**10 + ) + + # ------------------------------ 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) + + 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 + + # ------------------- 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. + + self.virtual_price = 10**18 * self.get_xcp(D) / self.totalSupply + self.xcp_profit_a = xcp_profit # <------------ Cache last claimed profit. + + +@internal +@view +def xp() -> uint256[N_COINS]: + + result: uint256[N_COINS] = self.balances + packed_prices: uint256 = self.price_scale_packed + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + + result[0] *= precisions[0] + for i in range(1, N_COINS): + p: uint256 = (packed_prices & PRICE_MASK) * precisions[i] + result[i] = result[i] * p / PRECISION + packed_prices = packed_prices >> PRICE_SIZE + + return result + + +@view +@internal +def _A_gamma() -> uint256[2]: + t1: uint256 = self.future_A_gamma_time + + A_gamma_1: uint256 = self.future_A_gamma + gamma1: uint256 = A_gamma_1 & 2**128 - 1 + A1: uint256 = A_gamma_1 >> 128 + + if block.timestamp < t1: + + # --------------- Handle ramping up and down of A -------------------- + + A_gamma_0: uint256 = self.initial_A_gamma + t0: uint256 = self.initial_A_gamma_time + + t1 -= t0 + t0 = block.timestamp - t0 + t2: uint256 = t1 - t0 + + A1 = ((A_gamma_0 >> 128) * t2 + A1 * t0) / t1 + gamma1 = ((A_gamma_0 & 2**128 - 1) * t2 + gamma1 * t0) / t1 + + return [A1, gamma1] + + +@internal +@view +def _fee(xp: uint256[N_COINS]) -> uint256: + fee_params: uint256[3] = self._unpack(self.packed_fee_params) + f: uint256 = MATH.reduction_coefficient(xp, fee_params[2]) + return unsafe_div( + fee_params[0] * f + fee_params[1] * (10**18 - f), + 10**18 + ) + + +@internal +@view +def get_xcp(D: 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 + # because we don't switch to "real" units. + + for i in range(1, N_COINS): + x[i] = D * 10**18 / (N_COINS * (packed_prices & PRICE_MASK)) + packed_prices = packed_prices >> PRICE_SIZE + + return MATH.geometric_mean(x) + + +@view +@internal +def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256: + # fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts) + fee: uint256 = unsafe_div( + unsafe_mul(self._fee(xp), N_COINS), + unsafe_mul(4, unsafe_sub(N_COINS, 1)) + ) + + S: uint256 = 0 + for _x in amounts: + S += _x + + avg: uint256 = unsafe_div(S, N_COINS) + Sdiff: uint256 = 0 + + for _x in amounts: + if _x > avg: + Sdiff += unsafe_sub(_x, avg) + else: + Sdiff += unsafe_sub(avg, _x) + + return fee * Sdiff / S + NOISE_FEE + + +@internal +@view +def _calc_withdraw_one_coin( + A_gamma: uint256[2], + token_amount: uint256, + i: uint256, + update_D: bool, +) -> (uint256, uint256, uint256[N_COINS], uint256): + + token_supply: uint256 = self.totalSupply + assert token_amount <= token_supply # dev: token amount more than supply + assert i < N_COINS # dev: coin out of range + + xx: uint256[N_COINS] = self.balances + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + xp: uint256[N_COINS] = precisions + D0: uint256 = 0 + + # -------------------------- Calculate D0 and xp ------------------------- + + 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): + p: uint256 = (packed_prices & PRICE_MASK) + if i == k: + price_scale_i = p * xp[i] + xp[k] = unsafe_div(xp[k] * xx[k] * p, PRECISION) + packed_prices = packed_prices >> PRICE_SIZE + + if update_D: # <-------------- D is updated if pool is undergoing a ramp. + D0 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + else: + D0 = self.D + + D: uint256 = D0 + + # -------------------------------- Fee Calc ------------------------------ + + # Charge fees on D. Roughly calculate xp[i] after withdrawal and use that + # to calculate fee. Precision is not paramount here: we just want a + # behavior where the higher the imbalance caused the more fee the AMM + # charges. + + # xp is adjusted assuming xp[0] ~= xp[1] ~= x[2], which is usually not the + # case. We charge self._fee(xp), where xp is an imprecise adjustment post + # withdrawal in one coin. If the withdraw is too large: charge max fee by + # default. This is because the fee calculation will otherwise underflow. + + xp_imprecise: uint256[N_COINS] = xp + xp_correction: uint256 = xp[i] * N_COINS * token_amount / token_supply + fee: uint256 = self._unpack(self.packed_fee_params)[1] # <- self.out_fee. + + if xp_correction < xp_imprecise[i]: + xp_imprecise[i] -= xp_correction + fee = self._fee(xp_imprecise) + + dD: uint256 = unsafe_div(token_amount * D, token_supply) + D_fee: uint256 = fee * dD / (2 * 10**10) + 1 # <------- Actual fee on D. + + # --------- Calculate `approx_fee` (assuming balanced state) in ith token. + # -------------------------------- We only need this for fee in the event. + approx_fee: uint256 = N_COINS * D_fee * xx[i] / D + + # ------------------------------------------------------------------------ + D -= (dD - D_fee) # <----------------------------------- Charge fee on D. + # --------------------------------- Calculate `y_out`` with `(D - D_fee)`. + y: uint256 = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, i)[0] + dy: uint256 = (xp[i] - y) * PRECISION / price_scale_i + xp[i] = y + + return dy, D, xp, approx_fee + + +# ------------------------ ERC20 functions ----------------------------------- + + +@internal +def _approve(_owner: address, _spender: address, _value: uint256): + self.allowance[_owner][_spender] = _value + + log Approval(_owner, _spender, _value) + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + assert _to not in [self, empty(address)] + + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + + log Transfer(_from, _to, _value) + + +@view +@internal +def _domain_separator() -> bytes32: + if chain.id != CACHED_CHAIN_ID: + return keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + return CACHED_DOMAIN_SEPARATOR + + +@external +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + """ + @dev Transfer tokens from one address to another. + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + @return bool True on successul transfer. Reverts otherwise. + """ + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != max_value(uint256): + self._approve(_from, msg.sender, _allowance - _value) + + self._transfer(_from, _to, _value) + return True + + +@external +def transfer(_to: address, _value: uint256) -> bool: + """ + @dev Transfer token for a specified address + @param _to The address to transfer to. + @param _value The amount to be transferred. + @return bool True on successful transfer. Reverts otherwise. + """ + self._transfer(msg.sender, _to, _value) + return True + + +@external +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. + @return bool Success + """ + self._approve(msg.sender, _spender, _value) + 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, + _spender: address, + _value: uint256, + _deadline: uint256, + _v: uint8, + _r: bytes32, + _s: bytes32, +) -> bool: + """ + @notice Permit `_spender` to spend up to `_value` amount of `_owner`'s + tokens via a signature. + @dev In the event of a chain fork, replay attacks are prevented as + domain separator is recalculated. However, this is only if the + resulting chains update their chainId. + @param _owner The account which generated the signature and is granting an + allowance. + @param _spender The account which will be granted an allowance. + @param _value The approval amount. + @param _deadline The deadline by which the signature must be submitted. + @param _v The last byte of the ECDSA signature. + @param _r The first 32 bytes of the ECDSA signature. + @param _s The second 32 bytes of the ECDSA signature. + @return bool Success. + """ + assert _owner != empty(address) # dev: invalid owner + assert block.timestamp <= _deadline # dev: permit expired + + nonce: uint256 = self.nonces[_owner] + digest: bytes32 = keccak256( + concat( + b"\x19\x01", + self._domain_separator(), + keccak256( + _abi_encode( + EIP2612_TYPEHASH, _owner, _spender, _value, nonce, _deadline + ) + ), + ) + ) + assert ecrecover(digest, _v, _r, _s) == _owner # dev: invalid signature + + self.nonces[_owner] = unsafe_add(nonce, 1) # <-- Unsafe add is safe here. + self._approve(_owner, _spender, _value) + return True + + +@internal +def mint(_to: address, _value: uint256) -> bool: + """ + @dev Mint an amount of the token and assigns it to an account. + This encapsulates the modification of balances such that the + proper events are emitted. + @param _to The account that will receive the created tokens. + @param _value The amount that will be created. + @return bool Success. + """ + self.totalSupply += _value + self.balanceOf[_to] += _value + + log Transfer(empty(address), _to, _value) + 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: + """ + @dev Burn an amount of the token from a given account. + @param _to The account whose tokens will be burned. + @param _value The amount that will be burned. + @return bool Success. + """ + self.totalSupply -= _value + self.balanceOf[_to] -= _value + + log Transfer(_to, empty(address), _value) + return True + + +# ------------------------- AMM View Functions ------------------------------- + + +@external +@view +def fee_receiver() -> address: + """ + @notice Returns the address of the admin fee receiver. + @return address Fee receiver. + """ + return Factory(self.factory).fee_receiver() + + +@external +@view +def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256: + """ + @notice Calculate LP tokens minted or to be burned for depositing or + removing `amounts` of coins + @dev Includes fee. + @param amounts Amounts of tokens being deposited or withdrawn + @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() + return Views(view_contract).calc_token_amount(amounts, deposit, self) + + +@external +@view +def get_dy(i: uint256, j: uint256, dx: uint256) -> uint256: + """ + @notice Get amount of coin[j] tokens received for swapping in dx amount of coin[i] + @dev Includes fee. + @param i index of input token. Check pool.coins(i) to get coin address at ith index + @param j index of output token + @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() + return Views(view_contract).get_dy(i, j, dx, self) + + +@external +@view +def get_dx(i: uint256, j: uint256, dy: uint256) -> uint256: + """ + @notice Get amount of coin[i] tokens to input for swapping out dy amount + of coin[j] + @dev This is an approximate method, and returns estimates close to the input + amount. Expensive to call on-chain. + @param i index of input token. Check pool.coins(i) to get coin address at + ith index + @param j index of output token + @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() + return Views(view_contract).get_dx(i, j, dy, self) + + +@external +@view +@nonreentrant("lock") +def lp_price() -> uint256: + """ + @notice Calculates the current price of the LP token w.r.t coin at the + 0th index + @return uint256 LP price. + """ + + 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 + + +@external +@view +@nonreentrant("lock") +def get_virtual_price() -> uint256: + """ + @notice Calculates the current virtual price of the pool LP token. + @dev Not to be confused with `self.virtual_price` which is a cached + virtual price. + @return uint256 Virtual Price. + """ + return 10**18 * self.get_xcp(self.D) / self.totalSupply + + +@external +@view +@nonreentrant("lock") +def price_oracle(k: uint256) -> uint256: + """ + @notice Returns the oracle price of the coin at index `k` w.r.t the coin + at index 0. + @dev The oracle is an exponential moving average, with a periodicity + determined by `self.ma_time`. The aggregated prices are cached state + prices (dy/dx) calculated AFTER the latest trade. + @param k The index of the coin. + @return uint256 Price oracle value of kth coin. + """ + price_oracle: uint256 = self._unpack_prices(self.price_oracle_packed)[k] + price_scale: uint256 = self._unpack_prices(self.price_scale_packed)[k] + last_prices_timestamp: uint256 = self.last_prices_timestamp + + if last_prices_timestamp < block.timestamp: # <------------ Update moving + # average if needed. + + last_prices: uint256 = self._unpack_prices(self.last_prices_packed)[k] + ma_time: uint256 = self._unpack(self.packed_rebalancing_params)[2] + alpha: uint256 = MATH.wad_exp( + -convert( + (block.timestamp - last_prices_timestamp) * 10**18 / ma_time, + int256, + ) + ) + + # ---- We cap state price that goes into the EMA with 2 x price_scale. + return ( + min(last_prices, 2 * price_scale) * (10**18 - alpha) + + price_oracle * alpha + ) / 10**18 + + return price_oracle + + +@external +@view +def last_prices(k: uint256) -> uint256: + """ + @notice Returns last price of the coin at index `k` w.r.t the coin + at index 0. + @dev last_prices returns the quote by the AMM for an infinitesimally small swap + after the last trade. It is not equivalent to the last traded price, and + is computed by taking the partial differential of `x` w.r.t `y`. The + derivative is calculated in `get_p` and then multiplied with price_scale + to give last_prices. + @param k The index of the coin. + @return uint256 Last logged price of coin. + """ + return self._unpack_prices(self.last_prices_packed)[k] + + +@external +@view +def price_scale(k: uint256) -> uint256: + """ + @notice Returns the price scale of the coin at index `k` w.r.t the coin + at index 0. + @dev Price scale determines the price band around which liquidity is + concentrated. + @param k The index of the coin. + @return uint256 Price scale of coin. + """ + return self._unpack_prices(self.price_scale_packed)[k] + + +@external +@view +def fee() -> uint256: + """ + @notice Returns the fee charged by the pool at current state. + @dev Not to be confused with the fee charged at liquidity action, since + there the fee is calculated on `xp` AFTER liquidity is added or + removed. + @return uint256 fee bps. + """ + return self._fee(self.xp()) + + +@view +@external +def calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256: + """ + @notice Calculates output tokens with fee + @param token_amount LP Token amount to burn + @param i token in which liquidity is withdrawn + @return uint256 Amount of ith tokens received for burning token_amount LP tokens. + """ + + return self._calc_withdraw_one_coin( + self._A_gamma(), + token_amount, + i, + (self.future_A_gamma_time > block.timestamp) + )[0] + + +@external +@view +def calc_token_fee( + amounts: uint256[N_COINS], xp: uint256[N_COINS] +) -> uint256: + """ + @notice Returns the fee charged on the given amounts for add_liquidity. + @param amounts The amounts of coins being added to the pool. + @param xp The current balances of the pool multiplied by coin precisions. + @return uint256 Fee charged. + """ + return self._calc_token_fee(amounts, xp) + + +@view +@external +def A() -> uint256: + """ + @notice Returns the current pool amplification parameter. + @return uint256 A param. + """ + return self._A_gamma()[0] + + +@view +@external +def gamma() -> uint256: + """ + @notice Returns the current pool gamma parameter. + @return uint256 gamma param. + """ + return self._A_gamma()[1] + + +@view +@external +def mid_fee() -> uint256: + """ + @notice Returns the current mid fee + @return uint256 mid_fee value. + """ + return self._unpack(self.packed_fee_params)[0] + + +@view +@external +def out_fee() -> uint256: + """ + @notice Returns the current out fee + @return uint256 out_fee value. + """ + return self._unpack(self.packed_fee_params)[1] + + +@view +@external +def fee_gamma() -> uint256: + """ + @notice Returns the current fee gamma + @return uint256 fee_gamma value. + """ + return self._unpack(self.packed_fee_params)[2] + + +@view +@external +def allowed_extra_profit() -> uint256: + """ + @notice Returns the current allowed extra profit + @return uint256 allowed_extra_profit value. + """ + return self._unpack(self.packed_rebalancing_params)[0] + + +@view +@external +def adjustment_step() -> uint256: + """ + @notice Returns the current adjustment step + @return uint256 adjustment_step value. + """ + return self._unpack(self.packed_rebalancing_params)[1] + + +@view +@external +def ma_time() -> uint256: + """ + @notice Returns the current moving average time in seconds + @dev To get time in seconds, the parameter is multipled by ln(2) + One can expect off-by-one errors here. + @return uint256 ma_time value. + """ + return self._unpack(self.packed_rebalancing_params)[2] * 694 / 1000 + + +@view +@external +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) + + +@external +@view +def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract. + """ + @notice Returns the fee charged by the pool at current state. + @param xp The current balances of the pool multiplied by coin precisions. + @return uint256 Fee value. + """ + return self._fee(xp) + + +@view +@external +def DOMAIN_SEPARATOR() -> bytes32: + """ + @notice EIP712 domain separator. + @return bytes32 Domain Separator set for the current chain. + """ + return self._domain_separator() + + +# ------------------------- AMM Admin Functions ------------------------------ + + +@external +def ramp_A_gamma( + future_A: uint256, future_gamma: uint256, future_time: uint256 +): + """ + @notice Initialise Ramping A and gamma parameter values linearly. + @dev Only accessible by factory admin, and only + @param future_A The future A value. + @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 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 + + A_gamma: uint256[2] = self._A_gamma() + initial_A_gamma: uint256 = A_gamma[0] << 128 + initial_A_gamma = initial_A_gamma | A_gamma[1] + + assert future_A > MIN_A - 1 + assert future_A < MAX_A + 1 + assert future_gamma > MIN_GAMMA - 1 + assert future_gamma < MAX_GAMMA + 1 + + ratio: uint256 = 10**18 * future_A / A_gamma[0] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + ratio = 10**18 * future_gamma / A_gamma[1] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + self.initial_A_gamma = initial_A_gamma + self.initial_A_gamma_time = block.timestamp + + future_A_gamma: uint256 = future_A << 128 + future_A_gamma = future_A_gamma | future_gamma + self.future_A_gamma_time = future_time + self.future_A_gamma = future_A_gamma + + log RampAgamma( + A_gamma[0], + future_A, + A_gamma[1], + future_gamma, + block.timestamp, + future_time, + ) + + +@external +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 + + A_gamma: uint256[2] = self._A_gamma() + current_A_gamma: uint256 = A_gamma[0] << 128 + current_A_gamma = current_A_gamma | A_gamma[1] + self.initial_A_gamma = current_A_gamma + self.future_A_gamma = current_A_gamma + self.initial_A_gamma_time = block.timestamp + self.future_A_gamma_time = block.timestamp + + # ------ Now (block.timestamp < t1) is always False, so we return saved A. + + log StopRampA(A_gamma[0], A_gamma[1], block.timestamp) + + +@external +def commit_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, +): + """ + @notice Commit new parameters. + @dev Only accessible by factory admin. + @param _new_mid_fee The new mid fee. + @param _new_out_fee The new out fee. + @param _new_fee_gamma The new fee gamma. + @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). + """ + assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert self.admin_actions_deadline == 0 # dev: active action + + _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY + self.admin_actions_deadline = _deadline + + # ----------------------------- Set fee params --------------------------- + + new_mid_fee: uint256 = _new_mid_fee + new_out_fee: uint256 = _new_out_fee + new_fee_gamma: uint256 = _new_fee_gamma + + current_fee_params: uint256[3] = self._unpack(self.packed_fee_params) + + if new_out_fee < MAX_FEE + 1: + assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range + else: + new_out_fee = current_fee_params[1] + + if new_mid_fee > MAX_FEE: + new_mid_fee = current_fee_params[0] + assert new_mid_fee <= new_out_fee # dev: mid-fee is too high + + if new_fee_gamma < 10**18: + assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18] + else: + new_fee_gamma = current_fee_params[2] + + self.future_packed_fee_params = self._pack( + [new_mid_fee, new_out_fee, new_fee_gamma] + ) + + # ----------------- Set liquidity rebalancing parameters ----------------- + + new_allowed_extra_profit: uint256 = _new_allowed_extra_profit + new_adjustment_step: uint256 = _new_adjustment_step + new_ma_time: uint256 = _new_ma_time + + current_rebalancing_params: uint256[3] = self._unpack(self.packed_rebalancing_params) + + if new_allowed_extra_profit > 10**18: + new_allowed_extra_profit = current_rebalancing_params[0] + + if new_adjustment_step > 10**18: + new_adjustment_step = current_rebalancing_params[1] + + if new_ma_time < 872542: # <----- Calculated as: 7 * 24 * 60 * 60 / ln(2) + assert new_ma_time > 86 # dev: MA time should be longer than 60/ln(2) + else: + new_ma_time = current_rebalancing_params[2] + + self.future_packed_rebalancing_params = self._pack( + [new_allowed_extra_profit, new_adjustment_step, new_ma_time] + ) + + # ---------------------------------- LOG --------------------------------- + + log CommitNewParameters( + _deadline, + new_mid_fee, + new_out_fee, + new_fee_gamma, + new_allowed_extra_profit, + new_adjustment_step, + new_ma_time, + ) + + +@external +@nonreentrant("lock") +def apply_new_parameters(): + """ + @notice Apply committed parameters. + @dev Only callable after admin_actions_deadline. + """ + assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time + assert self.admin_actions_deadline != 0 # dev: no active action + + self.admin_actions_deadline = 0 + + packed_fee_params: uint256 = self.future_packed_fee_params + self.packed_fee_params = packed_fee_params + + packed_rebalancing_params: uint256 = self.future_packed_rebalancing_params + self.packed_rebalancing_params = packed_rebalancing_params + + rebalancing_params: uint256[3] = self._unpack(packed_rebalancing_params) + fee_params: uint256[3] = self._unpack(packed_fee_params) + + log NewParameters( + fee_params[0], + fee_params[1], + fee_params[2], + rebalancing_params[0], + rebalancing_params[1], + rebalancing_params[2], + ) + + +@external +def revert_new_parameters(): + """ + @notice Revert committed parameters + @dev Only accessible by factory admin. Setting admin_actions_deadline to 0 + ensures a revert in apply_new_parameters. + """ + assert msg.sender == Factory(self.factory).admin() # dev: only owner + self.admin_actions_deadline = 0 diff --git a/deployments.yaml b/deployments.yaml index 36e4b17..7265668 100644 --- a/deployments.yaml +++ b/deployments.yaml @@ -1,113 +1,119 @@ arbitrum:mainnet: - amm_native_transfers_disabled: "0x1f7C86AffE5bCF7a1D74a8c8E2ef9E03BF31c1BD" - amm_native_transfers_enabled: "0xd7E72f3615aa65b92A4DBdC211E296a35512988B" - factory: "0xbC0797015fcFc47d9C1856639CaE50D0e69FbEE8" - math: "0x604388Bb1159AFd21eB5191cE22b4DeCdEE2Ae22" - views: "0x06452f9c013fc37169B57Eab8F50A7A48c9198A3" + amm_native_transfers_disabled: '0x1f7C86AffE5bCF7a1D74a8c8E2ef9E03BF31c1BD' + amm_native_transfers_enabled: '0xd7E72f3615aa65b92A4DBdC211E296a35512988B' + factory: '0xbC0797015fcFc47d9C1856639CaE50D0e69FbEE8' + math: '0x604388Bb1159AFd21eB5191cE22b4DeCdEE2Ae22' + views: '0x06452f9c013fc37169B57Eab8F50A7A48c9198A3' aurora:mainnet: - amm_native_transfers_disabled: "0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a" - amm_native_transfers_enabled: "0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf" - factory: "0xC1b393EfEF38140662b91441C6710Aa704973228" - math: "0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC" - views: "0xFAbC421e3368D158d802684A217a83c083c94CeB" + amm_native_transfers_disabled: '0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a' + amm_native_transfers_enabled: '0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf' + factory: '0xC1b393EfEF38140662b91441C6710Aa704973228' + math: '0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC' + views: '0xFAbC421e3368D158d802684A217a83c083c94CeB' avax:mainnet: - amm_native_transfers_disabled: "0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf" - amm_native_transfers_enabled: "0xFAbC421e3368D158d802684A217a83c083c94CeB" - factory: "0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a" - math: "0x505d666E4DD174DcDD7FA090ed95554486d2Be44" - views: "0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC" + amm_native_transfers_disabled: '0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf' + amm_native_transfers_enabled: '0xFAbC421e3368D158d802684A217a83c083c94CeB' + factory: '0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a' + math: '0x505d666E4DD174DcDD7FA090ed95554486d2Be44' + views: '0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC' base:mainnet: - amm_native_transfers_disabled: "0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf" - amm_native_transfers_enabled: "0xa274c88e09fDF1798a7517096557e6c1bEa1f65A" - factory: "0xA5961898870943c68037F6848d2D866Ed2016bcB" - math: "0x5373E1B9f2781099f6796DFe5D68DE59ac2F18E3" - views: "0x05d4E2Ed7216A204e5FB4e3F5187eCfaa5eF3Ef7" + amm_native_transfers_disabled: '0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf' + amm_native_transfers_enabled: '0xa274c88e09fDF1798a7517096557e6c1bEa1f65A' + factory: '0xA5961898870943c68037F6848d2D866Ed2016bcB' + math: '0x5373E1B9f2781099f6796DFe5D68DE59ac2F18E3' + views: '0x05d4E2Ed7216A204e5FB4e3F5187eCfaa5eF3Ef7' bsc:mainnet: - amm_native_transfers_disabled: "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf" - amm_native_transfers_enabled: "0xc6C09471Ee39C7E30a067952FcC89c8922f9Ab53" - factory: "0x38f8D93406fA2d9924DcFcB67dB5B0521Fb20F7D" - math: "0x0cE651Df1418a1fBA98517483102E042533Ade05" - views: "0x645E12f3cf5504C8a08e01706e79d3D0f32EcE15" + amm_native_transfers_disabled: '0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf' + amm_native_transfers_enabled: '0xc6C09471Ee39C7E30a067952FcC89c8922f9Ab53' + factory: '0x38f8D93406fA2d9924DcFcB67dB5B0521Fb20F7D' + math: '0x0cE651Df1418a1fBA98517483102E042533Ade05' + views: '0x645E12f3cf5504C8a08e01706e79d3D0f32EcE15' celo:mainnet: - amm_native_transfers_disabled: "0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf" - amm_native_transfers_enabled: "0xFAbC421e3368D158d802684A217a83c083c94CeB" - factory: "0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a" - math: "0x505d666E4DD174DcDD7FA090ed95554486d2Be44" - views: "0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC" + amm_native_transfers_disabled: '0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf' + amm_native_transfers_enabled: '0xFAbC421e3368D158d802684A217a83c083c94CeB' + factory: '0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a' + math: '0x505d666E4DD174DcDD7FA090ed95554486d2Be44' + views: '0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC' ethereum:mainnet: - amm_native_transfers_enabled: "0x66442B0C5260B92cAa9c234ECf2408CBf6b19a6f" - factory: "0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963" - math: "0xcBFf3004a20dBfE2731543AA38599A526e0fD6eE" - views: "0x064253915b8449fdEFac2c4A74aA9fdF56691a31" + amm_native_transfers_enabled: '0x66442B0C5260B92cAa9c234ECf2408CBf6b19a6f' + factory: '0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963' + math: '0xcBFf3004a20dBfE2731543AA38599A526e0fD6eE' + views: '0x064253915b8449fdEFac2c4A74aA9fdF56691a31' ethereum:sepolia: - amm_native_transfers_disabled: "0x3BbA971980A721C7A33cEF62cE01c0d744F26e95" - amm_native_transfers_enabled: "0xc9621394A73A071d8084CB9a15b04F182a7C9634" - factory: "0x4b00E8c997AeBACeEf6B8c6F89eE2bf99b2CA846" - math: "0x550574E33b81C45D3D69250b46Ae30c7bC40d330" - views: "0x59AfCD3e931018dc493AA1d833B11bb5A0744906" + amm_native_transfers_disabled: '0x3BbA971980A721C7A33cEF62cE01c0d744F26e95' + amm_native_transfers_enabled: '0xc9621394A73A071d8084CB9a15b04F182a7C9634' + factory: '0x4b00E8c997AeBACeEf6B8c6F89eE2bf99b2CA846' + math: '0x550574E33b81C45D3D69250b46Ae30c7bC40d330' + views: '0x59AfCD3e931018dc493AA1d833B11bb5A0744906' fraxtal:mainnet: - amm_native_transfers_disabled: "0x1A83348F9cCFD3Fe1A8C0adBa580Ac4e267Fe495" - amm_native_transfers_enabled: "0xd3b17f862956464ae4403ccf829ce69199856e1e" - factory: "0xc9Fe0C63Af9A39402e8a5514f9c43Af0322b665F" - math: "0x0C9D8c7e486e822C29488Ff51BFf0167B4650953" - views: "0x64379C265Fc6595065D7d835AAaa731c0584dB80" + amm_native_transfers_disabled: '0x1A83348F9cCFD3Fe1A8C0adBa580Ac4e267Fe495' + amm_native_transfers_enabled: '0xd3b17f862956464ae4403ccf829ce69199856e1e' + factory: '0xc9Fe0C63Af9A39402e8a5514f9c43Af0322b665F' + math: '0x0C9D8c7e486e822C29488Ff51BFf0167B4650953' + views: '0x64379C265Fc6595065D7d835AAaa731c0584dB80' ftm:mainnet: - amm_native_transfers_disabled: "0xd125E7a0cEddF89c6473412d85835450897be6Dc" - amm_native_transfers_enabled: "0x76303e4fDcA0AbF28aB3ee42Ce086E6503431F1D" - factory: "0x9AF14D26075f142eb3F292D5065EB3faa646167b" - math: "0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a" - views: "0xC1b393EfEF38140662b91441C6710Aa704973228" + amm_native_transfers_disabled: '0xd125E7a0cEddF89c6473412d85835450897be6Dc' + amm_native_transfers_enabled: '0x76303e4fDcA0AbF28aB3ee42Ce086E6503431F1D' + factory: '0x9AF14D26075f142eb3F292D5065EB3faa646167b' + math: '0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a' + views: '0xC1b393EfEF38140662b91441C6710Aa704973228' gnosis:mainnet: - amm_native_transfers_disabled: "0x3f445D38E820c010a7A6E33c5F80cBEBE6930f61" - amm_native_transfers_enabled: "0xa54f3c1dfa5f7dbf2564829d14b3b74a65d26ae2" - factory: "0xb47988ad49dce8d909c6f9cf7b26caf04e1445c8" - math: "0xff02cbd91f57a778bab7218da562594a680b8b61" - views: "0xe548590f9fAe7a23EA6501b144B0D58b74Fc4B53" + amm_native_transfers_disabled: '0x3f445D38E820c010a7A6E33c5F80cBEBE6930f61' + amm_native_transfers_enabled: '0xa54f3c1dfa5f7dbf2564829d14b3b74a65d26ae2' + factory: '0xb47988ad49dce8d909c6f9cf7b26caf04e1445c8' + math: '0xff02cbd91f57a778bab7218da562594a680b8b61' + views: '0xe548590f9fAe7a23EA6501b144B0D58b74Fc4B53' kava:mainnet: - amm_native_transfers_disabled: "0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf" - amm_native_transfers_enabled: "0xFAbC421e3368D158d802684A217a83c083c94CeB" - factory: "0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a" - math: "0x505d666E4DD174DcDD7FA090ed95554486d2Be44" - views: "0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC" + amm_native_transfers_disabled: '0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf' + amm_native_transfers_enabled: '0xFAbC421e3368D158d802684A217a83c083c94CeB' + factory: '0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a' + math: '0x505d666E4DD174DcDD7FA090ed95554486d2Be44' + views: '0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC' linea:mainnet: - amm_native_transfers_disabled: "0x76303e4fDcA0AbF28aB3ee42Ce086E6503431F1D" - amm_native_transfers_enabled: "0xC1b393EfEF38140662b91441C6710Aa704973228" - factory: "0xd125E7a0cEddF89c6473412d85835450897be6Dc" - math: "0x0C9D8c7e486e822C29488Ff51BFf0167B4650953" - views: "0x64379c265fc6595065d7d835aaaa731c0584db80" + amm_native_transfers_disabled: '0x76303e4fDcA0AbF28aB3ee42Ce086E6503431F1D' + amm_native_transfers_enabled: '0xC1b393EfEF38140662b91441C6710Aa704973228' + factory: '0xd125E7a0cEddF89c6473412d85835450897be6Dc' + math: '0x0C9D8c7e486e822C29488Ff51BFf0167B4650953' + views: '0x64379c265fc6595065d7d835aaaa731c0584db80' mantle:mainnet: - amm_native_transfers_disabled: "0x7Ca46A636b02D4aBC66883D7FF164bDE506DC66a" - amm_native_transfers_enabled: "0x046207cB759F527b6c10C2D61DBaca45513685CC" - factory: "0x0C9D8c7e486e822C29488Ff51BFf0167B4650953" - math: "0x635742dCC8313DCf8c904206037d962c042EAfBd" - views: "0x5702BDB1Ec244704E3cBBaAE11a0275aE5b07499" + amm_native_transfers_disabled: '0x7Ca46A636b02D4aBC66883D7FF164bDE506DC66a' + amm_native_transfers_enabled: '0x046207cB759F527b6c10C2D61DBaca45513685CC' + factory: '0x0C9D8c7e486e822C29488Ff51BFf0167B4650953' + math: '0x635742dCC8313DCf8c904206037d962c042EAfBd' + views: '0x5702BDB1Ec244704E3cBBaAE11a0275aE5b07499' optimism:mainnet: - amm_native_transfers_disabled: "0x0458ea5F4CD00E873264Be2031Ceb8f9d9b3116c" - amm_native_transfers_enabled: "0x1FE2a06c8bd81AE65FD1C5036451890b37976369" - factory: "0xc6C09471Ee39C7E30a067952FcC89c8922f9Ab53" - math: "0x19bd1AB34d6ABB584b9C1D5519093bfAA7f6c7d2" - views: "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf" + amm_native_transfers_disabled: '0x0458ea5F4CD00E873264Be2031Ceb8f9d9b3116c' + amm_native_transfers_enabled: '0x1FE2a06c8bd81AE65FD1C5036451890b37976369' + factory: '0xc6C09471Ee39C7E30a067952FcC89c8922f9Ab53' + math: '0x19bd1AB34d6ABB584b9C1D5519093bfAA7f6c7d2' + views: '0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf' polygon:mainnet: - amm_native_transfers_disabled: "0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a" - amm_native_transfers_enabled: "0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf" - factory: "0xC1b393EfEF38140662b91441C6710Aa704973228" - math: "0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC" - views: "0xFAbC421e3368D158d802684A217a83c083c94CeB" + amm_native_transfers_disabled: '0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a' + amm_native_transfers_enabled: '0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf' + factory: '0xC1b393EfEF38140662b91441C6710Aa704973228' + math: '0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC' + views: '0xFAbC421e3368D158d802684A217a83c083c94CeB' pzkevm:mainnet: - amm_native_transfers_disabled: "0xC1b393EfEF38140662b91441C6710Aa704973228" - amm_native_transfers_enabled: "0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a" - factory: "0x76303e4fDcA0AbF28aB3ee42Ce086E6503431F1D" - math: "0xFAbC421e3368D158d802684A217a83c083c94CeB" - views: "0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf" + amm_native_transfers_disabled: '0xC1b393EfEF38140662b91441C6710Aa704973228' + amm_native_transfers_enabled: '0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a' + factory: '0x76303e4fDcA0AbF28aB3ee42Ce086E6503431F1D' + math: '0xFAbC421e3368D158d802684A217a83c083c94CeB' + views: '0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf' scroll:mainnet: - amm_native_transfers_disabled: "0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a" - amm_native_transfers_enabled: "0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf" - factory: "0xC1b393EfEF38140662b91441C6710Aa704973228" - math: "0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC" - views: "0xFAbC421e3368D158d802684A217a83c083c94CeB" + amm_native_transfers_disabled: '0x3d6cB2F6DcF47CDd9C13E4e3beAe9af041d8796a' + amm_native_transfers_enabled: '0x0c59d36b23f809f8b6C7cb4c8C590a0AC103baEf' + factory: '0xC1b393EfEF38140662b91441C6710Aa704973228' + math: '0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC' + views: '0xFAbC421e3368D158d802684A217a83c083c94CeB' xlayer:mainnet: - amm_native_transfers_disabled: "0x64379C265Fc6595065D7d835AAaa731c0584dB80" - amm_native_transfers_enabled: "0x0C9D8c7e486e822C29488Ff51BFf0167B4650953" - factory: "0xd3B17f862956464ae4403cCF829CE69199856e1e" - math: "0x046207cB759F527b6c10C2D61DBaca45513685CC" - views: "0x7Ca46A636b02D4aBC66883D7FF164bDE506DC66a" + amm_native_transfers_disabled: '0x64379C265Fc6595065D7d835AAaa731c0584dB80' + amm_native_transfers_enabled: '0x0C9D8c7e486e822C29488Ff51BFf0167B4650953' + factory: '0xd3B17f862956464ae4403cCF829CE69199856e1e' + math: '0x046207cB759F527b6c10C2D61DBaca45513685CC' + views: '0x7Ca46A636b02D4aBC66883D7FF164bDE506DC66a' +zksync:mainnet: + amm_native_transfers_disabled: '0x46e1530c07D5BF2A5654C8cAAA60525D1a3f807A' + amm_native_transfers_enabled: '0xe1D19a2036BB6F78605cc6B0ac858C83196cAd22' + factory: '0x5044112fDf6c8DCc788a669c17345cfDB06549fa' + math: '0x07a1684378324825F67D92d944a713E2b8666DEb' + views: '0x30E9b9b8449056d17B33D5F42e1fdd5600A2397F' diff --git a/scripts/deploy_infra.py b/scripts/deploy_infra.py index 282e02a..5cb1657 100644 --- a/scripts/deploy_infra.py +++ b/scripts/deploy_infra.py @@ -4,6 +4,7 @@ import sys import boa +import boa_zksync import deployment_utils as deploy_utils import yaml from boa.network import NetworkEnv @@ -58,7 +59,29 @@ def check_and_deploy( logger.log(f"Deploying {contract_designation} contract ...") if blueprint: - c = contract_obj.deploy_as_blueprint() + if not "zksync" in network: + c = contract_obj.deploy_as_blueprint() + else: + # we need special deployment code for zksync + packed_precisions = 340282366920938463463374607431768211457 + packed_gamma_A = 136112946768375385385349842972852284582400000 + packed_fee_params = 8847341539944400050877843276543133320576000000 + packed_rebalancing_params = ( + 6125082604576892342340742933771827806226 + ) + c = contract_obj.deploy_as_blueprint( + "Blueprint", # _name + "_", # _symbol + ["0x0000000000000000000000000000000000000000"] * 3, # _coins + "0x0000000000000000000000000000000000000000", # _math + "0x0000000000000000000000000000000000000000", # _weth + b"\1" * 32, # _salt + packed_precisions, + packed_gamma_A, + packed_fee_params, + packed_rebalancing_params, + 1, # initial_price + ) else: c = contract_obj.deploy(*ctor_args) @@ -77,15 +100,29 @@ def check_and_deploy( def deploy_infra(network, url, account, fork=False): logger.log(f"Deploying on {network} ...") + contract_folder = "main" + + if network == "zksync:mainnet": + contract_folder = "zksync" + if not fork: + boa_zksync.set_zksync_env(url) + logger.log("Prodmode on zksync Era ...") + else: + boa_zksync.set_zksync_fork(url) + logger.log("Forkmode on zksync Era ...") + + boa.env.set_eoa(Account.from_key(os.environ[account])) - if fork: - boa.env.fork(url) - logger.log("Forkmode ...") - boa.env.eoa = deploy_utils.FIDDYDEPLOYER # set eoa address here else: - logger.log("Prodmode ...") - boa.set_env(NetworkEnv(url)) - boa.env.add_account(Account.from_key(os.environ[account])) + + if fork: + boa.env.fork(url) + logger.log("Forkmode ...") + boa.env.eoa = deploy_utils.FIDDYDEPLOYER # set eoa address here + else: + logger.log("Prodmode ...") + boa.set_env(NetworkEnv(url)) + boa.env.add_account(Account.from_key(os.environ[account])) # we want to deploy both implementations. ETH transfers implementation # goes to idx 0, and the no native token transfer version goes to idx 1. @@ -100,17 +137,17 @@ def deploy_infra(network, url, account, fork=False): # --------------------- Initialise contract objects --------------------- math_contract_obj = boa.load_partial( - "./contracts/main/CurveCryptoMathOptimized3.vy" + f"./contracts/{contract_folder}/CurveCryptoMathOptimized3.vy" ) views_contract_obj = boa.load_partial( - "./contracts/main/CurveCryptoViews3Optimized.vy" + f"./contracts/{contract_folder}/CurveCryptoViews3Optimized.vy" ) amm_contract_native_transfers_enabled_obj = boa.load_partial( - "./contracts/main/CurveTricryptoOptimizedWETH.vy" + f"./contracts/{contract_folder}/CurveTricryptoOptimizedWETH.vy" ) amm_contract_native_transfers_disabled_obj = boa.load_partial( - "./contracts/main/CurveTricryptoOptimized.vy" + f"./contracts/{contract_folder}/CurveTricryptoOptimized.vy" ) if network == "ethereum:mainnet": @@ -120,7 +157,7 @@ def deploy_infra(network, url, account, fork=False): logger.log("Using Mainnet tricrypto factory contract.") else: factory_contract_obj = boa.load_partial( - "./contracts/main/CurveL2TricryptoFactory.vy" + f"./contracts/{contract_folder}/CurveL2TricryptoFactory.vy" ) logger.log( "Using L2/sidechain (non-Ethereum mainnet) tricrypto factory contract." @@ -213,11 +250,14 @@ def deploy_infra(network, url, account, fork=False): def main(): - forkmode = True + forkmode = False + deployer = "FIDDYDEPLOYER" + network = "zksync:mainnet" + rpc = "https://mainnet.era.zksync.io" deploy_infra( - "", - "", - "FIDDYDEPLOYER", + network=network, + url=rpc, + account=deployer, fork=forkmode, )