From 8c60f7db77b02936e629e6ed0e9571b3b2f83ec8 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:46:00 +0200 Subject: [PATCH 1/3] add deployment script --- contracts/zksync/CurveCryptoMathOptimized2.vy | 579 +++++ .../zksync/CurveCryptoViews2Optimized.vy | 414 ++++ contracts/zksync/CurveTwocryptoFactory.vy | 483 +++++ contracts/zksync/CurveTwocryptoOptimized.vy | 1916 +++++++++++++++++ contracts/zksync/LiquidityGauge.vy | 865 ++++++++ scripts/deploy_infra.py | 58 +- scripts/deployment_utils.py | 4 +- 7 files changed, 4308 insertions(+), 11 deletions(-) create mode 100644 contracts/zksync/CurveCryptoMathOptimized2.vy create mode 100644 contracts/zksync/CurveCryptoViews2Optimized.vy create mode 100644 contracts/zksync/CurveTwocryptoFactory.vy create mode 100644 contracts/zksync/CurveTwocryptoOptimized.vy create mode 100644 contracts/zksync/LiquidityGauge.vy diff --git a/contracts/zksync/CurveCryptoMathOptimized2.vy b/contracts/zksync/CurveCryptoMathOptimized2.vy new file mode 100644 index 00000000..6774545c --- /dev/null +++ b/contracts/zksync/CurveCryptoMathOptimized2.vy @@ -0,0 +1,579 @@ +# pragma version 0.3.10 +# pragma evm-version paris +# (c) Curve.Fi, 2020-2023 +# AMM Math for 2-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 CurveTwocryptoMathOptimized +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Curve AMM Math for 2 unpegged assets (e.g. ETH <> USD). +""" + +N_COINS: constant(uint256) = 2 +A_MULTIPLIER: constant(uint256) = 10000 + +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA_SMALL: constant(uint256) = 2 * 10**16 +MAX_GAMMA: constant(uint256) = 199 * 10**15 # 1.99 * 10**17 + +MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 + +version: public(constant(String[8])) = "v2.1.0" + + +# ------------------------ AMM math functions -------------------------------- + + +@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 _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256, lim_mul: uint256) -> uint256: + """ + Calculating x[i] given other balances x[0..N_COINS-1] and invariant D + ANN = A * N**N + This is computationally expensive. + """ + + x_j: uint256 = x[1 - i] + y: uint256 = D**2 / (x_j * N_COINS**2) + K0_i: uint256 = (10**18 * N_COINS) * x_j / D + + assert (K0_i >= unsafe_div(10**36, lim_mul)) and (K0_i <= lim_mul) # dev: unsafe values x[i] + + convergence_limit: uint256 = max(max(x_j / 10**14, D / 10**14), 100) + + for j in range(255): + y_prev: uint256 = y + + K0: uint256 = K0_i * y * N_COINS / D + S: uint256 = x_j + y + + _g1k0: uint256 = gamma + 10**18 + if _g1k0 > K0: + _g1k0 = _g1k0 - K0 + 1 + else: + _g1k0 = K0 - _g1k0 + 1 + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1: uint256 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + + # 2*K0 / _g1k0 + mul2: uint256 = 10**18 + (2 * 10**18) * K0 / _g1k0 + + yfprime: uint256 = 10**18 * y + S * mul2 + mul1 + _dyfprime: uint256 = D * mul2 + if yfprime < _dyfprime: + y = y_prev / 2 + continue + else: + yfprime -= _dyfprime + fprime: uint256 = yfprime / y + + # y -= f / f_prime; y = (y * fprime - f) / fprime + # y = (yfprime + 10**18 * D - 10**18 * S) // fprime + mul1 // fprime * (10**18 - K0) // K0 + y_minus: uint256 = mul1 / fprime + y_plus: uint256 = (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 + + diff: uint256 = 0 + if y > y_prev: + diff = y - y_prev + else: + diff = y_prev - y + + if diff < max(convergence_limit, y / 10**14): + return y + + raise "Did not converge" + + +@external +@pure +def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256: + + # 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 + lim_mul: uint256 = 100 * 10**18 # 100.0 + if gamma > MAX_GAMMA_SMALL: + lim_mul = unsafe_div(unsafe_mul(lim_mul, MAX_GAMMA_SMALL), gamma) # smaller than 100.0 + + y: uint256 = self._newton_y(ANN, gamma, x, D, i, lim_mul) + frac: uint256 = y * 10**18 / D + assert (frac >= unsafe_div(10**36 / N_COINS, lim_mul)) and (frac <= unsafe_div(lim_mul, N_COINS)) # dev: unsafe value for y + + return y + + +@external +@pure +def get_y( + _ANN: uint256, + _gamma: uint256, + _x: uint256[N_COINS], + _D: uint256, + i: uint256 +) -> uint256[2]: + + # 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 + lim_mul: uint256 = 100 * 10**18 # 100.0 + if _gamma > MAX_GAMMA_SMALL: + lim_mul = unsafe_div(unsafe_mul(lim_mul, MAX_GAMMA_SMALL), _gamma) # smaller than 100.0 + lim_mul_signed: int256 = convert(lim_mul, int256) + + ANN: int256 = convert(_ANN, int256) + gamma: int256 = convert(_gamma, int256) + D: int256 = convert(_D, int256) + x_j: int256 = convert(_x[1 - i], int256) + gamma2: int256 = unsafe_mul(gamma, gamma) + + # savediv by x_j done here: + y: int256 = D**2 / (x_j * N_COINS**2) + + # K0_i: int256 = (10**18 * N_COINS) * x_j / D + K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D) + assert (K0_i >= unsafe_div(10**36, lim_mul_signed)) and (K0_i <= lim_mul_signed) # dev: unsafe values x[i] + + ann_gamma2: int256 = ANN * gamma2 + + # a = 10**36 / N_COINS**2 + a: int256 = 10**32 + + # b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14 + b: int256 = ( + D*ann_gamma2/400000000/x_j + - convert(unsafe_mul(10**32, 3), int256) + - unsafe_mul(unsafe_mul(2, gamma), 10**14) + ) + + # c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4 + c: int256 = ( + unsafe_mul(10**32, convert(3, int256)) + + unsafe_mul(unsafe_mul(4, gamma), 10**14) + + unsafe_div(gamma2, 10**4) + + unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D) + - unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) + ) + + # d = -(10**18+gamma)**2 / 10**4 + d: int256 = -unsafe_div(unsafe_add(10**18, gamma) ** 2, 10**4) + + # delta0: int256 = 3*a*c/b - b + delta0: int256 = 3 * a * c / b - b # safediv by b + + # delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b + delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b + + divider: int256 = 1 + threshold: int256 = min(min(abs(delta0), abs(delta1)), a) + if threshold > 10**48: + divider = 10**30 + elif threshold > 10**46: + divider = 10**28 + elif threshold > 10**44: + divider = 10**26 + elif threshold > 10**42: + divider = 10**24 + elif threshold > 10**40: + divider = 10**22 + elif threshold > 10**38: + divider = 10**20 + elif threshold > 10**36: + divider = 10**18 + elif threshold > 10**34: + divider = 10**16 + elif threshold > 10**32: + divider = 10**14 + elif threshold > 10**30: + divider = 10**12 + elif threshold > 10**28: + divider = 10**10 + elif threshold > 10**26: + divider = 10**8 + elif threshold > 10**24: + divider = 10**6 + elif threshold > 10**20: + divider = 10**2 + + a = unsafe_div(a, divider) + b = unsafe_div(b, divider) + c = unsafe_div(c, divider) + d = unsafe_div(d, divider) + + # delta0 = 3*a*c/b - b: here we can do more unsafe ops now: + delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b + + # delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b + delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b) + + # sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0 + sqrt_arg: int256 = delta1**2 + unsafe_mul(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, lim_mul), + 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: + # second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256) + second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256) + else: + # second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256) + second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256) + + # C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18 + C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18) + + # root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here. + root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a) + + # y_out: uint256[2] = [ + # convert(D**2/x_j*root/4/10**18, uint256), # <--- y + # convert(root, uint256) # <----------------------- K0Prev + # ] + y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)] + + frac: uint256 = unsafe_div(y_out[0] * 10**18, _D) + assert (frac >= unsafe_div(10**36 / N_COINS, lim_mul)) and (frac <= unsafe_div(lim_mul, N_COINS)) # dev: unsafe value for y + + return y_out + + +@external +@view +def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256: + """ + Finding the invariant using Newton method. + ANN is higher by the factor A_MULTIPLIER + ANN is already A * N**N + """ + + # 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 + + # Initial value of invariant D is that for constant-product invariant + x: uint256[N_COINS] = x_unsorted + if x[0] < x[1]: + x = [x_unsorted[1], x_unsorted[0]] + + assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0] + assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input) + + S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds + + D: uint256 = 0 + if K0_prev == 0: + D = N_COINS * isqrt(unsafe_mul(x[0], x[1])) + else: + # D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18) + D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18)) + if S < D: + D = S + + __g1k0: uint256 = gamma + 10**18 + diff: uint256 = 0 + + for i in range(255): + D_prev: uint256 = D + assert D > 0 + # Unsafe division by D and D_prev is now safe + + # K0: uint256 = 10**18 + # for _x in x: + # K0 = K0 * _x * N_COINS / D + # collapsed for 2 coins + K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D) + + _g1k0: uint256 = __g1k0 + if _g1k0 > K0: + _g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0 + else: + _g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0 + + # D / (A * N**N) * _g1k0**2 / gamma**2 + mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN) + + # 2*N*K0 / _g1k0 + mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0) + + # calculate neg_fprime. here K0 > 0 is being validated (safediv). + neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18) + + # D -= f / fprime; neg_fprime safediv being validated + D_plus: uint256 = D * (neg_fprime + S) / neg_fprime + D_minus: uint256 = unsafe_div(D * D, neg_fprime) + if 10**18 > K0: + D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0) + else: + D_minus -= unsafe_div(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) + 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) + + if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here + + for _x in x: + frac: uint256 = _x * 10**18 / D + assert (frac > 10**16 / N_COINS - 1) and (frac < 10**20 / N_COINS + 1) # dev: 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] +) -> uint256: + """ + @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(4 * _xp[0] * _xp[1], _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 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 + ) + + +@external +@pure +def wad_exp(x: int256) -> int256: + """ + @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. + @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 <= -42_139_678_854_452_767_551): + return empty(int256) + + # 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 < 135_305_999_368_893_231_589, "Math: 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, 54_916_777_467_707_473_351_141_471_128), 2 ** 95) >> 96 + value = unsafe_sub(value, unsafe_mul(k, 54_916_777_467_707_473_351_141_471_128)) + + # 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, 1_346_386_616_545_796_478_920_950_773_328), value) >> 96, 57_155_421_227_552_351_082_224_309_758_442) + p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94_201_549_194_550_492_254_356_042_504_812), y) >> 96,\ + 28_719_021_644_029_726_153_956_944_680_412_240), value), 4_385_272_521_454_847_904_659_076_985_693_276 << 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, 2_855_989_394_907_223_263_936_484_059_900), value) >> 96, 50_020_603_652_535_783_019_961_831_881_945) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 533_845_033_583_426_703_283_633_433_725_380) + q = unsafe_add(unsafe_mul(q, value) >> 96, 3_604_857_256_930_695_427_073_651_918_091_429) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 14_423_608_567_350_463_180_887_372_962_807_573) + q = unsafe_add(unsafe_mul(q, value) >> 96, 26_449_188_498_355_588_339_934_803_723_976_023) + + # 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 convert(unsafe_mul(convert(convert(r, bytes32), uint256), 3_822_833_074_963_236_453_042_738_258_902_158_003_155_416_615_667) >>\ + convert(unsafe_sub(195, k), uint256), int256) diff --git a/contracts/zksync/CurveCryptoViews2Optimized.vy b/contracts/zksync/CurveCryptoViews2Optimized.vy new file mode 100644 index 00000000..546c2396 --- /dev/null +++ b/contracts/zksync/CurveCryptoViews2Optimized.vy @@ -0,0 +1,414 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveCryptoViews2Optimized +@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() -> uint256: view + def price_oracle() -> 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 newton_y( + ANN: uint256, + gamma: uint256, + x: uint256[N_COINS], + D: uint256, + i: uint256, + ) -> uint256: view + + +N_COINS: constant(uint256) = 2 +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, + 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] + _xp[1] = _xp[1] * price_scale * precisions[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 = 0 + 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 = [xp[0] * precisions[0], xp[1] * price_scale * precisions[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 + 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 = 0 + 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 = [ + xp[0] * precisions[0], + xp[1] * price_scale * precisions[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 + 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 = 0 + 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 = [ + xp[0] * precisions[0], + xp[1] * price_scale * precisions[1] / PRECISION + ] + amountsp = [ + amountsp[0]* precisions[0], + amountsp[1] * price_scale * precisions[1] / 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]) + for k in range(N_COINS): + xx[k] = Curve(swap).balances(k) + + precisions: uint256[N_COINS] = Curve(swap).precisions() + A: uint256 = Curve(swap).A() + gamma: uint256 = Curve(swap).gamma() + D0: uint256 = 0 + p: uint256 = 0 + + price_scale_i: uint256 = Curve(swap).price_scale() * precisions[1] + xp: uint256[N_COINS] = [ + xx[0] * precisions[0], + unsafe_div(xx[1] * price_scale_i, PRECISION) + ] + if i == 0: + price_scale_i = PRECISION * precisions[0] + + 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: + + packed_fee_params: uint256 = Curve(swap).packed_fee_params() + fee_params: uint256[3] = self._unpack_3(packed_fee_params) + f: uint256 = xp[0] + xp[1] + f = fee_params[2] * 10**18 / ( + fee_params[2] + 10**18 - + (10**18 * N_COINS**N_COINS) * xp[0] / f * xp[1] / f + ) + + 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, + 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 = Curve(swap).price_scale() + + 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_3(_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/CurveTwocryptoFactory.vy b/contracts/zksync/CurveTwocryptoFactory.vy new file mode 100644 index 00000000..7da36716 --- /dev/null +++ b/contracts/zksync/CurveTwocryptoFactory.vy @@ -0,0 +1,483 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveTwocryptoFactory +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Permissionless 2-coin cryptoswap pool deployer and registry +""" + +interface TwocryptoPool: + def balances(i: uint256) -> uint256: view + +interface ERC20: + def decimals() -> uint256: view + + +event TwocryptoPoolDeployed: + pool: address + name: String[64] + symbol: String[32] + coins: address[N_COINS] + math: address + salt: bytes32 + precisions: uint256[N_COINS] + packed_A_gamma: uint256 + packed_fee_params: uint256 + packed_rebalancing_params: uint256 + packed_prices: uint256 + deployer: address + + +event LiquidityGaugeDeployed: + pool: address + gauge: 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 UpdateGaugeImplementation: + _old_gauge_implementation: address + _new_gauge_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: + liquidity_gauge: address + coins: address[N_COINS] + decimals: uint256[N_COINS] + implementation: address + + +N_COINS: constant(uint256) = 2 +A_MULTIPLIER: constant(uint256) = 10000 + +# Limits +MAX_FEE: constant(uint256) = 10 * 10 ** 9 + +deployer: address +admin: public(address) +future_admin: public(address) + +# fee receiver for all pools: +fee_receiver: public(address) + +pool_implementations: public(HashMap[uint256, address]) +gauge_implementation: public(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, DynArray[address, 4294967296]] +pool_data: HashMap[address, PoolArray] +pool_list: public(DynArray[address, 4294967296]) # master list of pools + + +@external +def __init__(): + self.deployer = tx.origin + + +@external +def initialise_ownership(_fee_receiver: address, _admin: address): + + assert msg.sender == self.deployer + assert self.admin == empty(address) + + self.fee_receiver = _fee_receiver + self.admin = _admin + + log UpdateFeeReceiver(empty(address), _fee_receiver) + log TransferOwnership(empty(address), _admin) + + +@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 The packed uint256 + """ + return (x[0] << 128) | (x[1] << 64) | x[2] + + +@pure +@internal +def _pack_2(p1: uint256, p2: uint256) -> uint256: + return p1 | (p2 << 128) + + +# <--- Pool Deployers ---> + +@external +def deploy_pool( + _name: String[64], + _symbol: String[32], + _coins: address[N_COINS], + 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_price: uint256, +) -> 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] + _math_implementation: address = self.math_implementation + assert pool_implementation != empty(address), "Pool implementation not set" + assert _math_implementation != empty(address), "Math implementation not set" + + 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 initial_price > 10**6 and initial_price < 10**30 # dev: initial price out of bound + + assert _coins[0] != _coins[1], "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 precision + packed_precisions: uint256 = self._pack_2(precisions[0], precisions[1]) + + # pack fees + packed_fee_params: uint256 = self._pack_3( + [mid_fee, out_fee, fee_gamma] + ) + + # pack liquidity rebalancing params + packed_rebalancing_params: uint256 = self._pack_3( + [allowed_extra_profit, adjustment_step, ma_exp_time] + ) + + # pack gamma and A + packed_gamma_A: uint256 = self._pack_2(gamma, A) + + # pool is an ERC20 implementation + _salt: bytes32 = block.prevhash + pool: address = create_from_blueprint( + pool_implementation, # blueprint: address + _name, # String[64] + _symbol, # String[32] + _coins, # address[N_COINS] + _math_implementation, # address + _salt, # bytes32 + packed_precisions, # uint256 + packed_gamma_A, # uint256 + packed_fee_params, # uint256 + packed_rebalancing_params, # uint256 + initial_price, # uint256 + code_offset=3, + ) + + # populate pool data + self.pool_list.append(pool) + + 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) + + log TwocryptoPoolDeployed( + pool, + _name, + _symbol, + _coins, + _math_implementation, + _salt, + precisions, + packed_gamma_A, + packed_fee_params, + packed_rebalancing_params, + initial_price, + 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) + ) + self.markets[key].append(pool) + + +@external +def deploy_gauge(_pool: address) -> address: + """ + @notice Deploy a liquidity gauge for a factory pool + @param _pool Factory pool address to deploy a gauge for + @return Address of the deployed gauge + """ + assert self.pool_data[_pool].coins[0] != empty(address), "Unknown pool" + assert self.pool_data[_pool].liquidity_gauge == empty(address), "Gauge already deployed" + assert self.gauge_implementation != empty(address), "Gauge implementation not set" + + gauge: address = create_from_blueprint(self.gauge_implementation, _pool, code_offset=3) + self.pool_data[_pool].liquidity_gauge = gauge + + log LiquidityGaugeDeployed(_pool, gauge) + return gauge + + +# <--- 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_gauge_implementation(_gauge_implementation: address): + """ + @notice Set gauge implementation + @dev Set to empty(address) to prevent deployment of new gauges + @param _gauge_implementation Address of the new token implementation + """ + assert msg.sender == self.admin, "dev: admin only" + + log UpdateGaugeImplementation(self.gauge_implementation, _gauge_implementation) + self.gauge_implementation = _gauge_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 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 pool_count() -> uint256: + """ + @notice Get number of pools deployed from the factory + @return Number of pools deployed from factory + """ + return len(self.pool_list) + + +@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 [TwocryptoPool(_pool).balances(0), TwocryptoPool(_pool).balances(1)] + + +@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[2] = self.pool_data[_pool].coins + + if _from == coins[0] and _to == coins[1]: + return 0, 1 + elif _from == coins[1] and _to == coins[0]: + return 1, 0 + else: + raise "Coins not found" + + +@view +@external +def get_gauge(_pool: address) -> address: + """ + @notice Get the address of the liquidity gauge contract for a factory pool + @dev Returns `empty(address)` if a gauge has not been deployed + @param _pool Pool address + @return Implementation contract address + """ + return self.pool_data[_pool].liquidity_gauge + + +@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 len(self.markets[key]) diff --git a/contracts/zksync/CurveTwocryptoOptimized.vy b/contracts/zksync/CurveTwocryptoOptimized.vy new file mode 100644 index 00000000..aeaf47a6 --- /dev/null +++ b/contracts/zksync/CurveTwocryptoOptimized.vy @@ -0,0 +1,1916 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveTwocryptoOptimized +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2023 - all rights reserved +@notice A Curve AMM pool for 2 unpegged assets (e.g. WETH, 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 wad_exp(_power: int256) -> 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: 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 + +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) = 2 +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)) + +cached_price_scale: uint256 # <------------------------ Internal price scale. +cached_price_oracle: uint256 # <------- Price target given by moving average. + +last_prices: public(uint256) +last_timestamp: public(uint256) # idx 0 is for prices, idx 1 is for xcp. + +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 / 10 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 +MAX_A_CHANGE: constant(uint256) = 10 +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA: constant(uint256) = 199 * 10**15 # 1.99 * 10**17 + +# ----------------------- ERC20 Specific vars -------------------------------- + +name: public(immutable(String[64])) +symbol: public(immutable(String[32])) +decimals: public(constant(uint8)) = 18 +version: public(constant(String[8])) = "v2.1.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, + _salt: bytes32, + packed_precisions: uint256, + packed_gamma_A: uint256, + packed_fee_params: uint256, + packed_rebalancing_params: uint256, + initial_price: uint256, +): + + MATH = Math(_math) + + factory = Factory(msg.sender) + name = _name + symbol = _symbol + coins = _coins + + PRECISIONS = self._unpack_2(packed_precisions) # <-- Precisions of coins. + + # --------------- Validate A and gamma parameters here and not in factory. + gamma_A: uint256[2] = self._unpack_2(packed_gamma_A) # gamma is at idx 0. + + assert gamma_A[0] > MIN_GAMMA-1 + assert gamma_A[0] < MAX_GAMMA+1 + + assert gamma_A[1] > MIN_A-1 + assert gamma_A[1] < MAX_A+1 + + self.initial_A_gamma = packed_gamma_A + self.future_A_gamma = packed_gamma_A + # ------------------------------------------------------------------------ + + 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.cached_price_scale = initial_price + self.cached_price_oracle = initial_price + self.last_prices = initial_price + self.last_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 ------------------ + + +@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] > 0 # dev: no coins to add + + # --------------------- Get prices, balances ----------------------------- + + price_scale: uint256 = self.cached_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 = [ + xp[0] * PRECISIONS[0], + unsafe_div(xp[1] * price_scale * PRECISIONS[1], PRECISION) + ] + xp_old = [ + xp_old[0] * PRECISIONS[0], + unsafe_div(xp_old[1] * price_scale * PRECISIONS[1], PRECISION) + ] + + for i in range(N_COINS): + 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, 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) + + 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 + + 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, + 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) + + 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] + + +# ---------------------- 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 + dy: uint256 = 0 + + y: uint256 = xp[j] + x0: uint256 = xp[i] - dx_received # old xp[i] + + price_scale: uint256 = self.cached_price_scale + xp = [ + xp[0] * PRECISIONS[0], + unsafe_div(xp[1] * price_scale * PRECISIONS[1], PRECISION) + ] + + # ----------- Update invariant if A, gamma are undergoing ramps --------- + + t: uint256 = self.future_A_gamma_time + if t > block.timestamp: + + x0 *= PRECISIONS[i] + + if i > 0: + x0 = unsafe_div(x0 * price_scale, 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 + 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, PRECISION) + xp[j] = y # <------------------------------------------------- Update xp. + + # ------ Tweak price_scale with good initial guess for newton_D ---------- + + price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1]) + + return [dy, fee, 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 = self.cached_price_oracle + last_prices: uint256 = self.last_prices + price_scale: uint256 = self.cached_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 Price Oracle if needed ----------------------- + + last_timestamp: uint256 = self.last_timestamp + alpha: uint256 = 0 + if last_timestamp < block.timestamp: # 0th index is for price_oracle. + + # The moving average price oracle is calculated using the last_price + # of the trade at the previous block, and the price oracle logged + # before that trade. This can happen only once per block. + + # ------------------ Calculate moving average params ----------------- + + alpha = MATH.wad_exp( + -convert( + unsafe_div( + unsafe_sub(block.timestamp, last_timestamp) * 10**18, + rebalancing_params[2] # <----------------------- ma_time. + ), + int256, + ) + ) + + # ---------------------------------------------- Update price oracles. + + # ----------------- We cap state price that goes into the EMA with + # 2 x price_scale. + price_oracle = unsafe_div( + min(last_prices, 2 * price_scale) * (10**18 - alpha) + + price_oracle * alpha, # ^-------- Cap spot price into EMA. + 10**18 + ) + + self.cached_price_oracle = price_oracle + self.last_timestamp = block.timestamp + + # `price_oracle` is used further on to calculate its vector distance from + # price_scale. This distance is used to calculate the amount of adjustment + # 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 -------------------------- + + self.last_prices = unsafe_div( + MATH.get_p(_xp, D_unadjusted, A_gamma) * price_scale, + 10**18 + ) + + # ---------- Update profit numbers without price adjustment first -------- + + xp: uint256[N_COINS] = [ + unsafe_div(D_unadjusted, N_COINS), + D_unadjusted * PRECISION / (N_COINS * price_scale) # <------ safediv. + ] # with price_scale. + + xcp_profit: uint256 = 10**18 + virtual_price: uint256 = 10**18 + + if old_virtual_price > 0: + + xcp: uint256 = isqrt(xp[0] * xp[1]) + 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: + # this usually reverts when withdrawing a very small amount of LP tokens + assert virtual_price > old_virtual_price # dev: virtual price decreased + + self.xcp_profit = xcp_profit + + # ------------ 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 = unsafe_div( + unsafe_mul(price_oracle, 10**18), price_scale + ) + if norm > 10**18: + norm = unsafe_sub(norm, 10**18) + else: + norm = unsafe_sub(10**18, norm) + 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 = unsafe_div( + price_scale * unsafe_sub(norm, adjustment_step) + + adjustment_step * price_oracle, + norm + ) # <---- norm is non-zero and gt adjustment_step; unsafe = safe. + + # ---------------- Update stale xp (using price_scale) with p_new. + + xp = [ + _xp[0], + unsafe_div(_xp[1] * p_new, price_scale) + ] + + # ------------------------------------------ Update D with new xp. + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + + # ------------------------------------- Convert xp to real prices. + xp = [ + unsafe_div(D, N_COINS), + D * PRECISION / (N_COINS * p_new) + ] + + # ---------- 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 * isqrt(xp[0] * xp[1]), 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 + ): + + self.D = D + self.virtual_price = old_virtual_price + self.cached_price_scale = p_new + + return p_new + + # --------- price_scale was not adjusted. Update the profit counter and D. + self.D = D_unadjusted + self.virtual_price = virtual_price + + return 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 + price_scale: uint256 = self.cached_price_scale + 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, 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: uint256, +) -> uint256[N_COINS]: + + return [ + balances[0] * PRECISIONS[0], + unsafe_div(balances[1] * PRECISIONS[1] * price_scale, PRECISION) + ] + + +@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 = xp[0] + xp[1] + f = fee_params[2] * 10**18 / ( + fee_params[2] + 10**18 - + (10**18 * N_COINS**N_COINS) * xp[0] / f * xp[1] / f + ) + + return unsafe_div( + fee_params[0] * f + fee_params[1] * (10**18 - f), + 10**18 + ) + + +@internal +@pure +def get_xcp(D: uint256, price_scale: uint256) -> uint256: + + x: uint256[N_COINS] = [ + unsafe_div(D, N_COINS), + D * PRECISION / (price_scale * N_COINS) + ] + + return isqrt(x[0] * x[1]) # <------------------- Geometric Mean. + + +@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 + D0: uint256 = 0 + + # -------------------------- Calculate D0 and xp ------------------------- + + price_scale_i: uint256 = self.cached_price_scale * PRECISIONS[1] + xp: uint256[N_COINS] = [ + xx[0] * PRECISIONS[0], + unsafe_div(xx[1] * price_scale_i, PRECISION) + ] + if i == 0: + price_scale_i = PRECISION * PRECISIONS[0] + + 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 # <------------------<---------- TODO: Check math. + + # ------------------------------------------------------------------------ + 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 ------------------------------- + + +@internal +@view +def internal_price_oracle() -> 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.cached_price_oracle + price_scale: uint256 = self.cached_price_scale + last_prices_timestamp: uint256 = self.last_timestamp + + if last_prices_timestamp < block.timestamp: # <------------ Update moving + # average if needed. + + last_prices: uint256 = self.last_prices + ma_time: uint256 = self._unpack_3(self.packed_rebalancing_params)[2] + alpha: uint256 = MATH.wad_exp( + -convert( + unsafe_sub(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 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. + """ + return 2 * self.virtual_price * isqrt(self.internal_price_oracle() * 10**18) / 10**18 + + +@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.cached_price_scale) / self.totalSupply + + +@external +@view +@nonreentrant("lock") +def price_oracle() -> 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. + @return uint256 Price oracle value of kth coin. + """ + return self.internal_price_oracle() + + +@external +@view +@nonreentrant("lock") +def price_scale() -> 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. + @return uint256 Price scale of coin. + """ + return self.cached_price_scale + + +@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.cached_price_scale)) + + +@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.future_A_gamma_time # dev: ramp undergoing + assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time + + A_gamma: uint256[2] = self._A_gamma() + 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 # dev: A change too high + assert ratio > 10**18 / MAX_A_CHANGE - 1 # dev: A change too low + + ratio = 10**18 * future_gamma / A_gamma[1] + assert ratio < 10**18 * MAX_A_CHANGE + 1 # dev: gamma change too high + assert ratio > 10**18 / MAX_A_CHANGE - 1 # dev: gamma change too low + + self.initial_A_gamma = initial_A_gamma + self.initial_A_gamma_time = block.timestamp + + 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, +): + """ + @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.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] + ) + + # ---------------------------------- LOG --------------------------------- + + log NewParameters( + new_mid_fee, + new_out_fee, + new_fee_gamma, + new_allowed_extra_profit, + new_adjustment_step, + new_ma_time, + ) diff --git a/contracts/zksync/LiquidityGauge.vy b/contracts/zksync/LiquidityGauge.vy new file mode 100644 index 00000000..19be9588 --- /dev/null +++ b/contracts/zksync/LiquidityGauge.vy @@ -0,0 +1,865 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title LiquidityGaugeV6 +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Implementation contract for use with Curve Factory +@dev Differs from v5.0.0 in that it uses create_from_blueprint to deploy Gauges +""" +from vyper.interfaces import ERC20 + +implements: ERC20 + + +interface CRV20: + def future_epoch_time_write() -> uint256: nonpayable + def rate() -> uint256: view + +interface Controller: + def checkpoint_gauge(addr: address): nonpayable + def gauge_relative_weight(addr: address, time: uint256) -> uint256: view + +interface ERC20Extended: + def symbol() -> String[32]: view + +interface ERC1271: + def isValidSignature(_hash: bytes32, _signature: Bytes[65]) -> bytes32: view + +interface Factory: + def admin() -> address: view + +interface Minter: + def minted(user: address, gauge: address) -> uint256: view + +interface VotingEscrow: + def user_point_epoch(addr: address) -> uint256: view + def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view + +interface VotingEscrowBoost: + def adjusted_balance_of(_account: address) -> uint256: view + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: indexed(address) + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event SetGaugeManager: + _gauge_manager: address + + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + +struct Reward: + token: address + distributor: address + period_finish: uint256 + rate: uint256 + last_update: uint256 + integral: uint256 + + +MAX_REWARDS: constant(uint256) = 8 +TOKENLESS_PRODUCTION: constant(uint256) = 40 +WEEK: constant(uint256) = 604800 + +VERSION: constant(String[8]) = "v6.1.0" # <- updated from v6.0.0 (makes rewards semi-permissionless) + +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) + +CRV: constant(address) = 0xD533a949740bb3306d119CC777fa900bA034cd52 +GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB +MINTER: constant(address) = 0xd061D61a4d941c39E5453435B6345Dc261C2fcE0 +VEBOOST_PROXY: constant(address) = 0x8E0c00ed546602fD9927DF742bbAbF726D5B0d16 +VOTING_ESCROW: constant(address) = 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2 + + +# ERC20 +balanceOf: public(HashMap[address, uint256]) +totalSupply: public(uint256) +allowance: public(HashMap[address, HashMap[address, uint256]]) + +name: public(immutable(String[64])) +symbol: public(immutable(String[40])) + +# ERC2612 +nonces: public(HashMap[address, uint256]) + +# Gauge +factory: public(immutable(address)) +lp_token: public(immutable(address)) +manager: public(address) + +is_killed: public(bool) + +# [future_epoch_time uint40][inflation_rate uint216] +inflation_params: uint256 + +# For tracking external rewards +reward_count: public(uint256) +reward_data: public(HashMap[address, Reward]) + +# claimant -> default reward receiver +rewards_receiver: public(HashMap[address, address]) + +# reward token -> claiming address -> integral +reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) + +# user -> [uint128 claimable amount][uint128 claimed amount] +claim_data: HashMap[address, HashMap[address, uint256]] + +working_balances: public(HashMap[address, uint256]) +working_supply: public(uint256) + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint +integrate_inv_supply_of: public(HashMap[address, uint256]) +integrate_checkpoint_of: public(HashMap[address, uint256]) + +# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint +# Units: rate * t = already number of coins per address to issue +integrate_fraction: public(HashMap[address, uint256]) + +# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint +# All values are kept in units of being multiplied by 1e18 +period: public(int128) + +# array of reward tokens +reward_tokens: public(address[MAX_REWARDS]) + +period_timestamp: public(uint256[100000000000000000000000000000]) +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint +integrate_inv_supply: public(uint256[100000000000000000000000000000]) # bump epoch when rate() changes + + +@external +def __init__(_lp_token: address): + """ + @notice Contract constructor + @param _lp_token Liquidity Pool contract address + """ + lp_token = _lp_token + factory = msg.sender + self.manager = msg.sender + + _symbol: String[32] = ERC20Extended(_lp_token).symbol() + _name: String[64] = concat("Curve.fi ", symbol, " Gauge Deposit") + + name = _name + symbol = concat(_symbol, "-gauge") + + self.period_timestamp[0] = block.timestamp + self.inflation_params = ( + (CRV20(CRV).future_epoch_time_write() << 216) + + CRV20(CRV).rate() + ) + + NAME_HASH = keccak256(_name) + salt = block.prevhash + CACHED_CHAIN_ID = chain.id + CACHED_DOMAIN_SEPARATOR = keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + + +# Internal Functions + +@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 + + +@internal +def _checkpoint(addr: address): + """ + @notice Checkpoint for a user + @dev Updates the CRV emissions a user is entitled to receive + @param addr User address + """ + _period: int128 = self.period + _period_time: uint256 = self.period_timestamp[_period] + _integrate_inv_supply: uint256 = self.integrate_inv_supply[_period] + + inflation_params: uint256 = self.inflation_params + prev_future_epoch: uint256 = inflation_params >> 216 + gauge_is_killed: bool = self.is_killed + + rate: uint256 = inflation_params % 2 ** 216 + new_rate: uint256 = rate + if gauge_is_killed: + rate = 0 + new_rate = 0 + + if prev_future_epoch >= _period_time: + future_epoch_time_write: uint256 = CRV20(CRV).future_epoch_time_write() + if not gauge_is_killed: + new_rate = CRV20(CRV).rate() + self.inflation_params = (future_epoch_time_write << 216) + new_rate + + # Update integral of 1/supply + if block.timestamp > _period_time: + _working_supply: uint256 = self.working_supply + Controller(GAUGE_CONTROLLER).checkpoint_gauge(self) + prev_week_time: uint256 = _period_time + week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp) + + for i in range(500): + dt: uint256 = week_time - prev_week_time + w: uint256 = Controller(GAUGE_CONTROLLER).gauge_relative_weight(self, prev_week_time) + + if _working_supply > 0: + if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time: + # If we went across one or multiple epochs, apply the rate + # of the first epoch until it ends, and then the rate of + # the last epoch. + # If more than one epoch is crossed - the gauge gets less, + # but that'd meen it wasn't called for more than 1 year + _integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply + rate = new_rate + _integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply + else: + _integrate_inv_supply += rate * w * dt / _working_supply + # On precisions of the calculation + # rate ~= 10e18 + # last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) + # _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) + # The largest loss is at dt = 1 + # Loss is 1e-9 - acceptable + + if week_time == block.timestamp: + break + prev_week_time = week_time + week_time = min(week_time + WEEK, block.timestamp) + + _period += 1 + self.period = _period + self.period_timestamp[_period] = block.timestamp + self.integrate_inv_supply[_period] = _integrate_inv_supply + + # Update user-specific integrals + _working_balance: uint256 = self.working_balances[addr] + self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18 + self.integrate_inv_supply_of[addr] = _integrate_inv_supply + self.integrate_checkpoint_of[addr] = block.timestamp + + +@internal +def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address): + """ + @notice Claim pending rewards and checkpoint rewards for a user + """ + + user_balance: uint256 = 0 + receiver: address = _receiver + if _user != empty(address): + user_balance = self.balanceOf[_user] + if _claim and _receiver == empty(address): + # if receiver is not explicitly declared, check if a default receiver is set + receiver = self.rewards_receiver[_user] + if receiver == empty(address): + # if no default receiver is set, direct claims to the user + receiver = _user + + reward_count: uint256 = self.reward_count + for i in range(MAX_REWARDS): + if i == reward_count: + break + token: address = self.reward_tokens[i] + + integral: uint256 = self.reward_data[token].integral + last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish) + duration: uint256 = last_update - self.reward_data[token].last_update + + if duration != 0 and _total_supply != 0: + self.reward_data[token].last_update = last_update + integral += duration * self.reward_data[token].rate * 10**18 / _total_supply + self.reward_data[token].integral = integral + + if _user != empty(address): + integral_for: uint256 = self.reward_integral_for[token][_user] + new_claimable: uint256 = 0 + + if integral_for < integral: + self.reward_integral_for[token][_user] = integral + new_claimable = user_balance * (integral - integral_for) / 10**18 + + claim_data: uint256 = self.claim_data[_user][token] + total_claimable: uint256 = (claim_data >> 128) + new_claimable + if total_claimable > 0: + total_claimed: uint256 = claim_data % 2**128 + if _claim: + assert ERC20(token).transfer(receiver, total_claimable, default_return_value=True) + self.claim_data[_user][token] = total_claimed + total_claimable + elif new_claimable > 0: + self.claim_data[_user][token] = total_claimed + (total_claimable << 128) + + +@internal +def _update_liquidity_limit(addr: address, l: uint256, L: uint256): + """ + @notice Calculate limits which depend on the amount of CRV token per-user. + Effectively it calculates working balances to apply amplification + of CRV production by CRV + @param addr User address + @param l User's amount of liquidity (LP tokens) + @param L Total amount of liquidity (LP tokens) + """ + # To be called after totalSupply is updated + voting_balance: uint256 = VotingEscrowBoost(VEBOOST_PROXY).adjusted_balance_of(addr) + voting_total: uint256 = ERC20(VOTING_ESCROW).totalSupply() + + lim: uint256 = l * TOKENLESS_PRODUCTION / 100 + if voting_total > 0: + lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 + + lim = min(l, lim) + old_bal: uint256 = self.working_balances[addr] + self.working_balances[addr] = lim + _working_supply: uint256 = self.working_supply + lim - old_bal + self.working_supply = _working_supply + + log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + """ + @notice Transfer tokens as well as checkpoint users + """ + self._checkpoint(_from) + self._checkpoint(_to) + + if _value != 0: + total_supply: uint256 = self.totalSupply + is_rewards: bool = self.reward_count != 0 + if is_rewards: + self._checkpoint_rewards(_from, total_supply, False, empty(address)) + new_balance: uint256 = self.balanceOf[_from] - _value + self.balanceOf[_from] = new_balance + self._update_liquidity_limit(_from, new_balance, total_supply) + + if is_rewards: + self._checkpoint_rewards(_to, total_supply, False, empty(address)) + new_balance = self.balanceOf[_to] + _value + self.balanceOf[_to] = new_balance + self._update_liquidity_limit(_to, new_balance, total_supply) + + log Transfer(_from, _to, _value) + + +# External User Facing Functions + + +@external +@nonreentrant('lock') +def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False): + """ + @notice Deposit `_value` LP tokens + @dev Depositting also claims pending reward tokens + @param _value Number of tokens to deposit + @param _addr Address to deposit for + """ + assert _addr != empty(address) # dev: cannot deposit for zero address + self._checkpoint(_addr) + + if _value != 0: + is_rewards: bool = self.reward_count != 0 + total_supply: uint256 = self.totalSupply + if is_rewards: + self._checkpoint_rewards(_addr, total_supply, _claim_rewards, empty(address)) + + total_supply += _value + new_balance: uint256 = self.balanceOf[_addr] + _value + self.balanceOf[_addr] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(_addr, new_balance, total_supply) + + ERC20(lp_token).transferFrom(msg.sender, self, _value) + + log Deposit(_addr, _value) + log Transfer(empty(address), _addr, _value) + + +@external +@nonreentrant('lock') +def withdraw(_value: uint256, _claim_rewards: bool = False): + """ + @notice Withdraw `_value` LP tokens + @dev Withdrawing also claims pending reward tokens + @param _value Number of tokens to withdraw + """ + self._checkpoint(msg.sender) + + if _value != 0: + is_rewards: bool = self.reward_count != 0 + total_supply: uint256 = self.totalSupply + if is_rewards: + self._checkpoint_rewards(msg.sender, total_supply, _claim_rewards, empty(address)) + + total_supply -= _value + new_balance: uint256 = self.balanceOf[msg.sender] - _value + self.balanceOf[msg.sender] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(msg.sender, new_balance, total_supply) + + ERC20(lp_token).transfer(msg.sender, _value) + + log Withdraw(msg.sender, _value) + log Transfer(msg.sender, empty(address), _value) + + +@external +@nonreentrant('lock') +def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(address)): + """ + @notice Claim available reward tokens for `_addr` + @param _addr Address to claim for + @param _receiver Address to transfer rewards to - if set to + empty(address), uses the default reward receiver + for the caller + """ + if _receiver != empty(address): + assert _addr == msg.sender # dev: cannot redirect when claiming for another user + self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) + + +@external +@nonreentrant('lock') +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + """ + @notice Transfer tokens from one address to another. + @dev Transferring claims pending reward tokens for the sender and receiver + @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 + """ + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != max_value(uint256): + _new_allowance: uint256 = _allowance - _value + self.allowance[_from][msg.sender] = _new_allowance + log Approval(_from, msg.sender, _new_allowance) + + self._transfer(_from, _to, _value) + + return True + + +@external +@nonreentrant('lock') +def transfer(_to: address, _value: uint256) -> bool: + """ + @notice Transfer token for a specified address + @dev Transferring claims pending reward tokens for the sender and receiver + @param _to The address to transfer to. + @param _value The amount to be transferred. + """ + self._transfer(msg.sender, _to, _value) + + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @notice Approve the passed address to transfer the specified amount of + tokens on behalf of msg.sender + @dev Beware that changing an allowance via this method brings the risk + that someone may use both the old and new allowance by unfortunate + transaction ordering. This may be mitigated with the use of + {incraseAllowance} and {decreaseAllowance}. + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will transfer the funds + @param _value The amount of tokens that may be transferred + @return bool success + """ + self.allowance[msg.sender][_spender] = _value + log Approval(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 Approves spender by owner's signature to expend owner's tokens. + See https://eips.ethereum.org/EIPS/eip-2612. + @dev Inspired by https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L753-L793 + @dev Supports smart contract wallets which implement ERC1271 + https://eips.ethereum.org/EIPS/eip-1271 + @param _owner The address which is a source of funds and has signed the Permit. + @param _spender The address which is allowed to spend the funds. + @param _value The amount of tokens to be spent. + @param _deadline The timestamp after which the Permit is no longer valid. + @param _v The bytes[64] of the valid secp256k1 signature of permit by owner + @param _r The bytes[0:32] of the valid secp256k1 signature of permit by owner + @param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner + @return True, if transaction completes successfully + """ + 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.allowance[_owner][_spender] = _value + self.nonces[_owner] = unsafe_add(nonce, 1) + + log Approval(_owner, _spender, _value) + return True + + +@external +def increaseAllowance(_spender: address, _added_value: uint256) -> bool: + """ + @notice Increase the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _added_value The amount of to increase the allowance + @return bool success + """ + allowance: uint256 = self.allowance[msg.sender][_spender] + _added_value + self.allowance[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: + """ + @notice Decrease the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _subtracted_value The amount of to decrease the allowance + @return bool success + """ + allowance: uint256 = self.allowance[msg.sender][_spender] - _subtracted_value + self.allowance[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +def user_checkpoint(addr: address) -> bool: + """ + @notice Record a checkpoint for `addr` + @param addr User address + @return bool success + """ + assert msg.sender in [addr, MINTER] # dev: unauthorized + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + return True + + +@external +def set_rewards_receiver(_receiver: address): + """ + @notice Set the default reward receiver for the caller. + @dev When set to empty(address), rewards are sent to the caller + @param _receiver Receiver address for any rewards claimed via `claim_rewards` + """ + self.rewards_receiver[msg.sender] = _receiver + + +@external +def kick(addr: address): + """ + @notice Kick `addr` for abusing their boost + @dev Only if either they had another voting event, or their voting escrow lock expired + @param addr Address to kick + """ + t_last: uint256 = self.integrate_checkpoint_of[addr] + t_ve: uint256 = VotingEscrow(VOTING_ESCROW).user_point_history__ts( + addr, VotingEscrow(VOTING_ESCROW).user_point_epoch(addr) + ) + _balance: uint256 = self.balanceOf[addr] + + assert ERC20(VOTING_ESCROW).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed + assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed + + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + + +# Administrative Functions + + +@external +def set_gauge_manager(_gauge_manager: address): + """ + @notice Change the gauge manager for a gauge + @dev The manager of this contract, or the ownership admin can outright modify gauge + managership. A gauge manager can also transfer managership to a new manager via this + method, but only for the gauge which they are the manager of. + @param _gauge_manager The account to set as the new manager of the gauge. + """ + assert msg.sender in [self.manager, Factory(factory).admin()] # dev: only manager or factory admin + + self.manager = _gauge_manager + log SetGaugeManager(_gauge_manager) + + +@external +@nonreentrant("lock") +def deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK): + """ + @notice Deposit a reward token for distribution + @param _reward_token The reward token being deposited + @param _amount The amount of `_reward_token` being deposited + @param _epoch The duration the rewards are distributed across. + """ + assert msg.sender == self.reward_data[_reward_token].distributor + + self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) + + # transferFrom reward token and use transferred amount henceforth: + amount_received: uint256 = ERC20(_reward_token).balanceOf(self) + assert ERC20(_reward_token).transferFrom( + msg.sender, + self, + _amount, + default_return_value=True + ) + amount_received = ERC20(_reward_token).balanceOf(self) - amount_received + + period_finish: uint256 = self.reward_data[_reward_token].period_finish + assert amount_received > _epoch # dev: rate will tend to zero! + + if block.timestamp >= period_finish: + self.reward_data[_reward_token].rate = amount_received / _epoch # TODO: consider using precision here hmm + else: + remaining: uint256 = period_finish - block.timestamp + leftover: uint256 = remaining * self.reward_data[_reward_token].rate + self.reward_data[_reward_token].rate = (amount_received + leftover) / _epoch + + self.reward_data[_reward_token].last_update = block.timestamp + self.reward_data[_reward_token].period_finish = block.timestamp + _epoch + + +@external +def add_reward(_reward_token: address, _distributor: address): + """ + @notice Add additional rewards to be distributed to stakers + @param _reward_token The token to add as an additional reward + @param _distributor Address permitted to fund this contract with the reward token + """ + assert msg.sender in [self.manager, Factory(factory).admin()] # dev: only manager or factory admin + assert _distributor != empty(address) # dev: distributor cannot be zero address + + reward_count: uint256 = self.reward_count + assert reward_count < MAX_REWARDS + assert self.reward_data[_reward_token].distributor == empty(address) + + self.reward_data[_reward_token].distributor = _distributor + self.reward_tokens[reward_count] = _reward_token + self.reward_count = reward_count + 1 + + +@external +def set_reward_distributor(_reward_token: address, _distributor: address): + """ + @notice Reassign the reward distributor for a reward token + @param _reward_token The reward token to reassign distribution rights to + @param _distributor The address of the new distributor + """ + current_distributor: address = self.reward_data[_reward_token].distributor + + assert msg.sender in [current_distributor, Factory(factory).admin(), self.manager] + assert current_distributor != empty(address) + assert _distributor != empty(address) + + self.reward_data[_reward_token].distributor = _distributor + + +@external +def set_killed(_is_killed: bool): + """ + @notice Set the killed status for this contract + @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV + @param _is_killed Killed status to set + """ + assert msg.sender == Factory(factory).admin() # dev: only owner + + self.is_killed = _is_killed + + +# View Methods + + +@view +@external +def claimed_reward(_addr: address, _token: address) -> uint256: + """ + @notice Get the number of already-claimed reward tokens for a user + @param _addr Account to get reward amount for + @param _token Token to get reward amount for + @return uint256 Total amount of `_token` already claimed by `_addr` + """ + return self.claim_data[_addr][_token] % 2**128 + + +@view +@external +def claimable_reward(_user: address, _reward_token: address) -> uint256: + """ + @notice Get the number of claimable reward tokens for a user + @param _user Account to get reward amount for + @param _reward_token Token to get reward amount for + @return uint256 Claimable reward token amount + """ + integral: uint256 = self.reward_data[_reward_token].integral + total_supply: uint256 = self.totalSupply + if total_supply != 0: + last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish) + duration: uint256 = last_update - self.reward_data[_reward_token].last_update + integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply) + + integral_for: uint256 = self.reward_integral_for[_reward_token][_user] + new_claimable: uint256 = self.balanceOf[_user] * (integral - integral_for) / 10**18 + + return (self.claim_data[_user][_reward_token] >> 128) + new_claimable + + +@external +def claimable_tokens(addr: address) -> uint256: + """ + @notice Get the number of claimable tokens per user + @dev This function should be manually changed to "view" in the ABI + @return uint256 number of claimable tokens per user + """ + self._checkpoint(addr) + return self.integrate_fraction[addr] - Minter(MINTER).minted(addr, self) + + +@view +@external +def integrate_checkpoint() -> uint256: + """ + @notice Get the timestamp of the last checkpoint + """ + return self.period_timestamp[self.period] + + +@view +@external +def future_epoch_time() -> uint256: + """ + @notice Get the locally stored CRV future epoch start time + """ + return self.inflation_params >> 216 + + +@view +@external +def inflation_rate() -> uint256: + """ + @notice Get the locally stored CRV inflation rate + """ + return self.inflation_params % 2 ** 216 + + +@view +@external +def decimals() -> uint256: + """ + @notice Get the number of decimals for this token + @dev Implemented as a view method to reduce gas costs + @return uint256 decimal places + """ + return 18 + + +@view +@external +def version() -> String[8]: + """ + @notice Get the version of this gauge contract + """ + return VERSION + + +@view +@external +def DOMAIN_SEPARATOR() -> bytes32: + """ + @notice EIP712 domain separator. + """ + return self._domain_separator() diff --git a/scripts/deploy_infra.py b/scripts/deploy_infra.py index 2a535dcd..ca773aaf 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 @@ -83,13 +84,37 @@ def check_and_deploy( ) deployed_address = precomputed_address - except DecodeError: + except: logger.log( f"No create2deployer found for {network}. Deploying with CREATE." ) 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"] + * 2, # _coins + "0x0000000000000000000000000000000000000000", # _math + b"\1" * 32, # _salt + packed_precisions, + packed_gamma_A, + packed_fee_params, + packed_rebalancing_params, + 1, # initial_price + ) else: c = contract_obj.deploy() @@ -108,6 +133,18 @@ 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) @@ -132,16 +169,16 @@ def deploy_infra(network, url, account, fork=False): # --------------------- Initialise contract objects --------------------- math_contract_obj = boa.load_partial( - "./contracts/main/CurveCryptoMathOptimized2.vy" + f"./contracts/{contract_folder}/CurveCryptoMathOptimized2.vy" ) views_contract_obj = boa.load_partial( - "./contracts/main/CurveCryptoViews2Optimized.vy" + f"./contracts/{contract_folder}/CurveCryptoViews2Optimized.vy" ) amm_contract_obj = boa.load_partial( - "./contracts/main/CurveTwocryptoOptimized.vy" + f"./contracts/{contract_folder}/CurveTwocryptoOptimized.vy" ) factory_contract_obj = boa.load_partial( - "./contracts/main/CurveTwocryptoFactory.vy" + f"./contracts/{contract_folder}/CurveTwocryptoFactory.vy" ) # deploy non-blueprint contracts: @@ -214,10 +251,13 @@ def deploy_infra(network, url, account, fork=False): def main(): forkmode = False + deployer = "FIDDYDEPLOYER" + network = "zksync:mainnet" + rpc = "https://mainnet.era.zksync.io" deploy_infra( - "", - "", - "", + network=network, + url=rpc, + account=deployer, fork=forkmode, ) diff --git a/scripts/deployment_utils.py b/scripts/deployment_utils.py index 20b3bc81..4d8d7457 100644 --- a/scripts/deployment_utils.py +++ b/scripts/deployment_utils.py @@ -110,8 +110,8 @@ class CurveNetworkSettings: fee_receiver_address="", ), "zksync:mainnet": CurveNetworkSettings( - thin_proxy_contract="", - fee_receiver_address="0x4920088D9a5e5De9c098FCA4960d0DA5f4caa4c1", + thin_proxy_contract="0xCb8799BFF48bb549F7B69Bb9BE60DbA7cd4F1BB7", + fee_receiver_address="0xCb8799BFF48bb549F7B69Bb9BE60DbA7cd4F1BB7", ), "fraxtal:mainnet": CurveNetworkSettings( thin_proxy_contract="0x8b3EFBEfa6eD222077455d6f0DCdA3bF4f3F57A6", From 7e983374d9a4709306a9e7a5f3f5ee419a068ea9 Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:46:12 +0200 Subject: [PATCH 2/3] add deployment script --- contracts/zksync/LiquidityGauge.vy | 865 ----------------------------- 1 file changed, 865 deletions(-) delete mode 100644 contracts/zksync/LiquidityGauge.vy diff --git a/contracts/zksync/LiquidityGauge.vy b/contracts/zksync/LiquidityGauge.vy deleted file mode 100644 index 19be9588..00000000 --- a/contracts/zksync/LiquidityGauge.vy +++ /dev/null @@ -1,865 +0,0 @@ -# pragma version 0.3.10 -# pragma evm-version paris -""" -@title LiquidityGaugeV6 -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -@notice Implementation contract for use with Curve Factory -@dev Differs from v5.0.0 in that it uses create_from_blueprint to deploy Gauges -""" -from vyper.interfaces import ERC20 - -implements: ERC20 - - -interface CRV20: - def future_epoch_time_write() -> uint256: nonpayable - def rate() -> uint256: view - -interface Controller: - def checkpoint_gauge(addr: address): nonpayable - def gauge_relative_weight(addr: address, time: uint256) -> uint256: view - -interface ERC20Extended: - def symbol() -> String[32]: view - -interface ERC1271: - def isValidSignature(_hash: bytes32, _signature: Bytes[65]) -> bytes32: view - -interface Factory: - def admin() -> address: view - -interface Minter: - def minted(user: address, gauge: address) -> uint256: view - -interface VotingEscrow: - def user_point_epoch(addr: address) -> uint256: view - def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view - -interface VotingEscrowBoost: - def adjusted_balance_of(_account: address) -> uint256: view - - -event Deposit: - provider: indexed(address) - value: uint256 - -event Withdraw: - provider: indexed(address) - value: uint256 - -event UpdateLiquidityLimit: - user: indexed(address) - original_balance: uint256 - original_supply: uint256 - working_balance: uint256 - working_supply: uint256 - -event CommitOwnership: - admin: address - -event ApplyOwnership: - admin: address - -event SetGaugeManager: - _gauge_manager: address - - -event Transfer: - _from: indexed(address) - _to: indexed(address) - _value: uint256 - -event Approval: - _owner: indexed(address) - _spender: indexed(address) - _value: uint256 - - -struct Reward: - token: address - distributor: address - period_finish: uint256 - rate: uint256 - last_update: uint256 - integral: uint256 - - -MAX_REWARDS: constant(uint256) = 8 -TOKENLESS_PRODUCTION: constant(uint256) = 40 -WEEK: constant(uint256) = 604800 - -VERSION: constant(String[8]) = "v6.1.0" # <- updated from v6.0.0 (makes rewards semi-permissionless) - -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) - -CRV: constant(address) = 0xD533a949740bb3306d119CC777fa900bA034cd52 -GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB -MINTER: constant(address) = 0xd061D61a4d941c39E5453435B6345Dc261C2fcE0 -VEBOOST_PROXY: constant(address) = 0x8E0c00ed546602fD9927DF742bbAbF726D5B0d16 -VOTING_ESCROW: constant(address) = 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2 - - -# ERC20 -balanceOf: public(HashMap[address, uint256]) -totalSupply: public(uint256) -allowance: public(HashMap[address, HashMap[address, uint256]]) - -name: public(immutable(String[64])) -symbol: public(immutable(String[40])) - -# ERC2612 -nonces: public(HashMap[address, uint256]) - -# Gauge -factory: public(immutable(address)) -lp_token: public(immutable(address)) -manager: public(address) - -is_killed: public(bool) - -# [future_epoch_time uint40][inflation_rate uint216] -inflation_params: uint256 - -# For tracking external rewards -reward_count: public(uint256) -reward_data: public(HashMap[address, Reward]) - -# claimant -> default reward receiver -rewards_receiver: public(HashMap[address, address]) - -# reward token -> claiming address -> integral -reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) - -# user -> [uint128 claimable amount][uint128 claimed amount] -claim_data: HashMap[address, HashMap[address, uint256]] - -working_balances: public(HashMap[address, uint256]) -working_supply: public(uint256) - -# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint -integrate_inv_supply_of: public(HashMap[address, uint256]) -integrate_checkpoint_of: public(HashMap[address, uint256]) - -# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint -# Units: rate * t = already number of coins per address to issue -integrate_fraction: public(HashMap[address, uint256]) - -# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint -# All values are kept in units of being multiplied by 1e18 -period: public(int128) - -# array of reward tokens -reward_tokens: public(address[MAX_REWARDS]) - -period_timestamp: public(uint256[100000000000000000000000000000]) -# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint -integrate_inv_supply: public(uint256[100000000000000000000000000000]) # bump epoch when rate() changes - - -@external -def __init__(_lp_token: address): - """ - @notice Contract constructor - @param _lp_token Liquidity Pool contract address - """ - lp_token = _lp_token - factory = msg.sender - self.manager = msg.sender - - _symbol: String[32] = ERC20Extended(_lp_token).symbol() - _name: String[64] = concat("Curve.fi ", symbol, " Gauge Deposit") - - name = _name - symbol = concat(_symbol, "-gauge") - - self.period_timestamp[0] = block.timestamp - self.inflation_params = ( - (CRV20(CRV).future_epoch_time_write() << 216) - + CRV20(CRV).rate() - ) - - NAME_HASH = keccak256(_name) - salt = block.prevhash - CACHED_CHAIN_ID = chain.id - CACHED_DOMAIN_SEPARATOR = keccak256( - _abi_encode( - EIP712_TYPEHASH, - NAME_HASH, - VERSION_HASH, - chain.id, - self, - salt, - ) - ) - - -# Internal Functions - -@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 - - -@internal -def _checkpoint(addr: address): - """ - @notice Checkpoint for a user - @dev Updates the CRV emissions a user is entitled to receive - @param addr User address - """ - _period: int128 = self.period - _period_time: uint256 = self.period_timestamp[_period] - _integrate_inv_supply: uint256 = self.integrate_inv_supply[_period] - - inflation_params: uint256 = self.inflation_params - prev_future_epoch: uint256 = inflation_params >> 216 - gauge_is_killed: bool = self.is_killed - - rate: uint256 = inflation_params % 2 ** 216 - new_rate: uint256 = rate - if gauge_is_killed: - rate = 0 - new_rate = 0 - - if prev_future_epoch >= _period_time: - future_epoch_time_write: uint256 = CRV20(CRV).future_epoch_time_write() - if not gauge_is_killed: - new_rate = CRV20(CRV).rate() - self.inflation_params = (future_epoch_time_write << 216) + new_rate - - # Update integral of 1/supply - if block.timestamp > _period_time: - _working_supply: uint256 = self.working_supply - Controller(GAUGE_CONTROLLER).checkpoint_gauge(self) - prev_week_time: uint256 = _period_time - week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp) - - for i in range(500): - dt: uint256 = week_time - prev_week_time - w: uint256 = Controller(GAUGE_CONTROLLER).gauge_relative_weight(self, prev_week_time) - - if _working_supply > 0: - if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time: - # If we went across one or multiple epochs, apply the rate - # of the first epoch until it ends, and then the rate of - # the last epoch. - # If more than one epoch is crossed - the gauge gets less, - # but that'd meen it wasn't called for more than 1 year - _integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply - rate = new_rate - _integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply - else: - _integrate_inv_supply += rate * w * dt / _working_supply - # On precisions of the calculation - # rate ~= 10e18 - # last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) - # _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) - # The largest loss is at dt = 1 - # Loss is 1e-9 - acceptable - - if week_time == block.timestamp: - break - prev_week_time = week_time - week_time = min(week_time + WEEK, block.timestamp) - - _period += 1 - self.period = _period - self.period_timestamp[_period] = block.timestamp - self.integrate_inv_supply[_period] = _integrate_inv_supply - - # Update user-specific integrals - _working_balance: uint256 = self.working_balances[addr] - self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18 - self.integrate_inv_supply_of[addr] = _integrate_inv_supply - self.integrate_checkpoint_of[addr] = block.timestamp - - -@internal -def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address): - """ - @notice Claim pending rewards and checkpoint rewards for a user - """ - - user_balance: uint256 = 0 - receiver: address = _receiver - if _user != empty(address): - user_balance = self.balanceOf[_user] - if _claim and _receiver == empty(address): - # if receiver is not explicitly declared, check if a default receiver is set - receiver = self.rewards_receiver[_user] - if receiver == empty(address): - # if no default receiver is set, direct claims to the user - receiver = _user - - reward_count: uint256 = self.reward_count - for i in range(MAX_REWARDS): - if i == reward_count: - break - token: address = self.reward_tokens[i] - - integral: uint256 = self.reward_data[token].integral - last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish) - duration: uint256 = last_update - self.reward_data[token].last_update - - if duration != 0 and _total_supply != 0: - self.reward_data[token].last_update = last_update - integral += duration * self.reward_data[token].rate * 10**18 / _total_supply - self.reward_data[token].integral = integral - - if _user != empty(address): - integral_for: uint256 = self.reward_integral_for[token][_user] - new_claimable: uint256 = 0 - - if integral_for < integral: - self.reward_integral_for[token][_user] = integral - new_claimable = user_balance * (integral - integral_for) / 10**18 - - claim_data: uint256 = self.claim_data[_user][token] - total_claimable: uint256 = (claim_data >> 128) + new_claimable - if total_claimable > 0: - total_claimed: uint256 = claim_data % 2**128 - if _claim: - assert ERC20(token).transfer(receiver, total_claimable, default_return_value=True) - self.claim_data[_user][token] = total_claimed + total_claimable - elif new_claimable > 0: - self.claim_data[_user][token] = total_claimed + (total_claimable << 128) - - -@internal -def _update_liquidity_limit(addr: address, l: uint256, L: uint256): - """ - @notice Calculate limits which depend on the amount of CRV token per-user. - Effectively it calculates working balances to apply amplification - of CRV production by CRV - @param addr User address - @param l User's amount of liquidity (LP tokens) - @param L Total amount of liquidity (LP tokens) - """ - # To be called after totalSupply is updated - voting_balance: uint256 = VotingEscrowBoost(VEBOOST_PROXY).adjusted_balance_of(addr) - voting_total: uint256 = ERC20(VOTING_ESCROW).totalSupply() - - lim: uint256 = l * TOKENLESS_PRODUCTION / 100 - if voting_total > 0: - lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 - - lim = min(l, lim) - old_bal: uint256 = self.working_balances[addr] - self.working_balances[addr] = lim - _working_supply: uint256 = self.working_supply + lim - old_bal - self.working_supply = _working_supply - - log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) - - -@internal -def _transfer(_from: address, _to: address, _value: uint256): - """ - @notice Transfer tokens as well as checkpoint users - """ - self._checkpoint(_from) - self._checkpoint(_to) - - if _value != 0: - total_supply: uint256 = self.totalSupply - is_rewards: bool = self.reward_count != 0 - if is_rewards: - self._checkpoint_rewards(_from, total_supply, False, empty(address)) - new_balance: uint256 = self.balanceOf[_from] - _value - self.balanceOf[_from] = new_balance - self._update_liquidity_limit(_from, new_balance, total_supply) - - if is_rewards: - self._checkpoint_rewards(_to, total_supply, False, empty(address)) - new_balance = self.balanceOf[_to] + _value - self.balanceOf[_to] = new_balance - self._update_liquidity_limit(_to, new_balance, total_supply) - - log Transfer(_from, _to, _value) - - -# External User Facing Functions - - -@external -@nonreentrant('lock') -def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False): - """ - @notice Deposit `_value` LP tokens - @dev Depositting also claims pending reward tokens - @param _value Number of tokens to deposit - @param _addr Address to deposit for - """ - assert _addr != empty(address) # dev: cannot deposit for zero address - self._checkpoint(_addr) - - if _value != 0: - is_rewards: bool = self.reward_count != 0 - total_supply: uint256 = self.totalSupply - if is_rewards: - self._checkpoint_rewards(_addr, total_supply, _claim_rewards, empty(address)) - - total_supply += _value - new_balance: uint256 = self.balanceOf[_addr] + _value - self.balanceOf[_addr] = new_balance - self.totalSupply = total_supply - - self._update_liquidity_limit(_addr, new_balance, total_supply) - - ERC20(lp_token).transferFrom(msg.sender, self, _value) - - log Deposit(_addr, _value) - log Transfer(empty(address), _addr, _value) - - -@external -@nonreentrant('lock') -def withdraw(_value: uint256, _claim_rewards: bool = False): - """ - @notice Withdraw `_value` LP tokens - @dev Withdrawing also claims pending reward tokens - @param _value Number of tokens to withdraw - """ - self._checkpoint(msg.sender) - - if _value != 0: - is_rewards: bool = self.reward_count != 0 - total_supply: uint256 = self.totalSupply - if is_rewards: - self._checkpoint_rewards(msg.sender, total_supply, _claim_rewards, empty(address)) - - total_supply -= _value - new_balance: uint256 = self.balanceOf[msg.sender] - _value - self.balanceOf[msg.sender] = new_balance - self.totalSupply = total_supply - - self._update_liquidity_limit(msg.sender, new_balance, total_supply) - - ERC20(lp_token).transfer(msg.sender, _value) - - log Withdraw(msg.sender, _value) - log Transfer(msg.sender, empty(address), _value) - - -@external -@nonreentrant('lock') -def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(address)): - """ - @notice Claim available reward tokens for `_addr` - @param _addr Address to claim for - @param _receiver Address to transfer rewards to - if set to - empty(address), uses the default reward receiver - for the caller - """ - if _receiver != empty(address): - assert _addr == msg.sender # dev: cannot redirect when claiming for another user - self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) - - -@external -@nonreentrant('lock') -def transferFrom(_from: address, _to: address, _value: uint256) -> bool: - """ - @notice Transfer tokens from one address to another. - @dev Transferring claims pending reward tokens for the sender and receiver - @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 - """ - _allowance: uint256 = self.allowance[_from][msg.sender] - if _allowance != max_value(uint256): - _new_allowance: uint256 = _allowance - _value - self.allowance[_from][msg.sender] = _new_allowance - log Approval(_from, msg.sender, _new_allowance) - - self._transfer(_from, _to, _value) - - return True - - -@external -@nonreentrant('lock') -def transfer(_to: address, _value: uint256) -> bool: - """ - @notice Transfer token for a specified address - @dev Transferring claims pending reward tokens for the sender and receiver - @param _to The address to transfer to. - @param _value The amount to be transferred. - """ - self._transfer(msg.sender, _to, _value) - - return True - - -@external -def approve(_spender : address, _value : uint256) -> bool: - """ - @notice Approve the passed address to transfer the specified amount of - tokens on behalf of msg.sender - @dev Beware that changing an allowance via this method brings the risk - that someone may use both the old and new allowance by unfortunate - transaction ordering. This may be mitigated with the use of - {incraseAllowance} and {decreaseAllowance}. - https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - @param _spender The address which will transfer the funds - @param _value The amount of tokens that may be transferred - @return bool success - """ - self.allowance[msg.sender][_spender] = _value - log Approval(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 Approves spender by owner's signature to expend owner's tokens. - See https://eips.ethereum.org/EIPS/eip-2612. - @dev Inspired by https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L753-L793 - @dev Supports smart contract wallets which implement ERC1271 - https://eips.ethereum.org/EIPS/eip-1271 - @param _owner The address which is a source of funds and has signed the Permit. - @param _spender The address which is allowed to spend the funds. - @param _value The amount of tokens to be spent. - @param _deadline The timestamp after which the Permit is no longer valid. - @param _v The bytes[64] of the valid secp256k1 signature of permit by owner - @param _r The bytes[0:32] of the valid secp256k1 signature of permit by owner - @param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner - @return True, if transaction completes successfully - """ - 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.allowance[_owner][_spender] = _value - self.nonces[_owner] = unsafe_add(nonce, 1) - - log Approval(_owner, _spender, _value) - return True - - -@external -def increaseAllowance(_spender: address, _added_value: uint256) -> bool: - """ - @notice Increase the allowance granted to `_spender` by the caller - @dev This is alternative to {approve} that can be used as a mitigation for - the potential race condition - @param _spender The address which will transfer the funds - @param _added_value The amount of to increase the allowance - @return bool success - """ - allowance: uint256 = self.allowance[msg.sender][_spender] + _added_value - self.allowance[msg.sender][_spender] = allowance - - log Approval(msg.sender, _spender, allowance) - - return True - - -@external -def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: - """ - @notice Decrease the allowance granted to `_spender` by the caller - @dev This is alternative to {approve} that can be used as a mitigation for - the potential race condition - @param _spender The address which will transfer the funds - @param _subtracted_value The amount of to decrease the allowance - @return bool success - """ - allowance: uint256 = self.allowance[msg.sender][_spender] - _subtracted_value - self.allowance[msg.sender][_spender] = allowance - - log Approval(msg.sender, _spender, allowance) - - return True - - -@external -def user_checkpoint(addr: address) -> bool: - """ - @notice Record a checkpoint for `addr` - @param addr User address - @return bool success - """ - assert msg.sender in [addr, MINTER] # dev: unauthorized - self._checkpoint(addr) - self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) - return True - - -@external -def set_rewards_receiver(_receiver: address): - """ - @notice Set the default reward receiver for the caller. - @dev When set to empty(address), rewards are sent to the caller - @param _receiver Receiver address for any rewards claimed via `claim_rewards` - """ - self.rewards_receiver[msg.sender] = _receiver - - -@external -def kick(addr: address): - """ - @notice Kick `addr` for abusing their boost - @dev Only if either they had another voting event, or their voting escrow lock expired - @param addr Address to kick - """ - t_last: uint256 = self.integrate_checkpoint_of[addr] - t_ve: uint256 = VotingEscrow(VOTING_ESCROW).user_point_history__ts( - addr, VotingEscrow(VOTING_ESCROW).user_point_epoch(addr) - ) - _balance: uint256 = self.balanceOf[addr] - - assert ERC20(VOTING_ESCROW).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed - assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed - - self._checkpoint(addr) - self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) - - -# Administrative Functions - - -@external -def set_gauge_manager(_gauge_manager: address): - """ - @notice Change the gauge manager for a gauge - @dev The manager of this contract, or the ownership admin can outright modify gauge - managership. A gauge manager can also transfer managership to a new manager via this - method, but only for the gauge which they are the manager of. - @param _gauge_manager The account to set as the new manager of the gauge. - """ - assert msg.sender in [self.manager, Factory(factory).admin()] # dev: only manager or factory admin - - self.manager = _gauge_manager - log SetGaugeManager(_gauge_manager) - - -@external -@nonreentrant("lock") -def deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK): - """ - @notice Deposit a reward token for distribution - @param _reward_token The reward token being deposited - @param _amount The amount of `_reward_token` being deposited - @param _epoch The duration the rewards are distributed across. - """ - assert msg.sender == self.reward_data[_reward_token].distributor - - self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - - # transferFrom reward token and use transferred amount henceforth: - amount_received: uint256 = ERC20(_reward_token).balanceOf(self) - assert ERC20(_reward_token).transferFrom( - msg.sender, - self, - _amount, - default_return_value=True - ) - amount_received = ERC20(_reward_token).balanceOf(self) - amount_received - - period_finish: uint256 = self.reward_data[_reward_token].period_finish - assert amount_received > _epoch # dev: rate will tend to zero! - - if block.timestamp >= period_finish: - self.reward_data[_reward_token].rate = amount_received / _epoch # TODO: consider using precision here hmm - else: - remaining: uint256 = period_finish - block.timestamp - leftover: uint256 = remaining * self.reward_data[_reward_token].rate - self.reward_data[_reward_token].rate = (amount_received + leftover) / _epoch - - self.reward_data[_reward_token].last_update = block.timestamp - self.reward_data[_reward_token].period_finish = block.timestamp + _epoch - - -@external -def add_reward(_reward_token: address, _distributor: address): - """ - @notice Add additional rewards to be distributed to stakers - @param _reward_token The token to add as an additional reward - @param _distributor Address permitted to fund this contract with the reward token - """ - assert msg.sender in [self.manager, Factory(factory).admin()] # dev: only manager or factory admin - assert _distributor != empty(address) # dev: distributor cannot be zero address - - reward_count: uint256 = self.reward_count - assert reward_count < MAX_REWARDS - assert self.reward_data[_reward_token].distributor == empty(address) - - self.reward_data[_reward_token].distributor = _distributor - self.reward_tokens[reward_count] = _reward_token - self.reward_count = reward_count + 1 - - -@external -def set_reward_distributor(_reward_token: address, _distributor: address): - """ - @notice Reassign the reward distributor for a reward token - @param _reward_token The reward token to reassign distribution rights to - @param _distributor The address of the new distributor - """ - current_distributor: address = self.reward_data[_reward_token].distributor - - assert msg.sender in [current_distributor, Factory(factory).admin(), self.manager] - assert current_distributor != empty(address) - assert _distributor != empty(address) - - self.reward_data[_reward_token].distributor = _distributor - - -@external -def set_killed(_is_killed: bool): - """ - @notice Set the killed status for this contract - @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV - @param _is_killed Killed status to set - """ - assert msg.sender == Factory(factory).admin() # dev: only owner - - self.is_killed = _is_killed - - -# View Methods - - -@view -@external -def claimed_reward(_addr: address, _token: address) -> uint256: - """ - @notice Get the number of already-claimed reward tokens for a user - @param _addr Account to get reward amount for - @param _token Token to get reward amount for - @return uint256 Total amount of `_token` already claimed by `_addr` - """ - return self.claim_data[_addr][_token] % 2**128 - - -@view -@external -def claimable_reward(_user: address, _reward_token: address) -> uint256: - """ - @notice Get the number of claimable reward tokens for a user - @param _user Account to get reward amount for - @param _reward_token Token to get reward amount for - @return uint256 Claimable reward token amount - """ - integral: uint256 = self.reward_data[_reward_token].integral - total_supply: uint256 = self.totalSupply - if total_supply != 0: - last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish) - duration: uint256 = last_update - self.reward_data[_reward_token].last_update - integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply) - - integral_for: uint256 = self.reward_integral_for[_reward_token][_user] - new_claimable: uint256 = self.balanceOf[_user] * (integral - integral_for) / 10**18 - - return (self.claim_data[_user][_reward_token] >> 128) + new_claimable - - -@external -def claimable_tokens(addr: address) -> uint256: - """ - @notice Get the number of claimable tokens per user - @dev This function should be manually changed to "view" in the ABI - @return uint256 number of claimable tokens per user - """ - self._checkpoint(addr) - return self.integrate_fraction[addr] - Minter(MINTER).minted(addr, self) - - -@view -@external -def integrate_checkpoint() -> uint256: - """ - @notice Get the timestamp of the last checkpoint - """ - return self.period_timestamp[self.period] - - -@view -@external -def future_epoch_time() -> uint256: - """ - @notice Get the locally stored CRV future epoch start time - """ - return self.inflation_params >> 216 - - -@view -@external -def inflation_rate() -> uint256: - """ - @notice Get the locally stored CRV inflation rate - """ - return self.inflation_params % 2 ** 216 - - -@view -@external -def decimals() -> uint256: - """ - @notice Get the number of decimals for this token - @dev Implemented as a view method to reduce gas costs - @return uint256 decimal places - """ - return 18 - - -@view -@external -def version() -> String[8]: - """ - @notice Get the version of this gauge contract - """ - return VERSION - - -@view -@external -def DOMAIN_SEPARATOR() -> bytes32: - """ - @notice EIP712 domain separator. - """ - return self._domain_separator() From 1ffb4e3d92d21b8eb1921c4a6be3aa88df15b31d Mon Sep 17 00:00:00 2001 From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:53:10 +0200 Subject: [PATCH 3/3] deployed! --- deployments.yaml | 5 +++++ scripts/deploy_infra.py | 16 +++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/deployments.yaml b/deployments.yaml index 0b88f6d7..902d3468 100644 --- a/deployments.yaml +++ b/deployments.yaml @@ -130,3 +130,8 @@ xlayer:mainnet: math: '0x1Fd8Af16DC4BEBd950521308D55d0543b6cDF4A1' math_v2_0_0: '0x505d666E4DD174DcDD7FA090ed95554486d2Be44' views: '0x5a8C93EE12a8Df4455BA111647AdA41f29D5CfcC' +zksync:mainnet: + amm: '0x69949489645190D5C4e57a5B9e57705C57033EBb' + factory: '0x24992A09E2257AF325102Cefa1F09E80E9062d49' + math: '0x5AF4Fa25F76491F949C648AC439b1953df78f594' + views: '0xfe30c516c23504B6CF740de513390DC6943888d8' diff --git a/scripts/deploy_infra.py b/scripts/deploy_infra.py index ca773aaf..68d7560d 100644 --- a/scripts/deploy_infra.py +++ b/scripts/deploy_infra.py @@ -146,14 +146,16 @@ def deploy_infra(network, url, account, fork=False): 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])) CREATE2DEPLOYER = boa.load_abi("abi/create2deployer.json").at( "0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2"