From 57e6f0c42c46a394eb634d2b357b2330250ace8b Mon Sep 17 00:00:00 2001 From: Alberto Date: Sat, 29 Jun 2024 20:37:00 +0200 Subject: [PATCH] test: script to analyze pool behavior --- .../xcp_profit_rebalancing/CurveTwocrypto.vy | 1917 +++++++++++++++++ .../CurveTwocryptoFactory.vy | 484 +++++ .../CurveTwocryptoMath.vy | 580 +++++ .../CurveTwocryptoViews.vy | 415 ++++ .../old/xcp_profit_rebalancing/README.md | 3 + scripts/experiments/rebalance_playground.py | 252 +++ 6 files changed, 3651 insertions(+) create mode 100644 contracts/old/xcp_profit_rebalancing/CurveTwocrypto.vy create mode 100644 contracts/old/xcp_profit_rebalancing/CurveTwocryptoFactory.vy create mode 100644 contracts/old/xcp_profit_rebalancing/CurveTwocryptoMath.vy create mode 100644 contracts/old/xcp_profit_rebalancing/CurveTwocryptoViews.vy create mode 100644 contracts/old/xcp_profit_rebalancing/README.md create mode 100644 scripts/experiments/rebalance_playground.py diff --git a/contracts/old/xcp_profit_rebalancing/CurveTwocrypto.vy b/contracts/old/xcp_profit_rebalancing/CurveTwocrypto.vy new file mode 100644 index 00000000..899c0159 --- /dev/null +++ b/contracts/old/xcp_profit_rebalancing/CurveTwocrypto.vy @@ -0,0 +1,1917 @@ +# pragma version 0.3.10 +# pragma optimize gas +# 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.0.0" + +balanceOf: public(HashMap[address, uint256]) +allowance: public(HashMap[address, HashMap[address, uint256]]) +totalSupply: public(uint256) +nonces: public(HashMap[address, uint256]) + +EIP712_TYPEHASH: constant(bytes32) = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" +) +EIP2612_TYPEHASH: constant(bytes32) = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +) +VERSION_HASH: constant(bytes32) = keccak256(version) +NAME_HASH: immutable(bytes32) +CACHED_CHAIN_ID: immutable(uint256) +salt: public(immutable(bytes32)) +CACHED_DOMAIN_SEPARATOR: immutable(bytes32) + + +# ----------------------- Contract ------------------------------------------- + +@external +def __init__( + _name: String[64], + _symbol: String[32], + _coins: address[N_COINS], + _math: address, + _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/old/xcp_profit_rebalancing/CurveTwocryptoFactory.vy b/contracts/old/xcp_profit_rebalancing/CurveTwocryptoFactory.vy new file mode 100644 index 00000000..6c1f754d --- /dev/null +++ b/contracts/old/xcp_profit_rebalancing/CurveTwocryptoFactory.vy @@ -0,0 +1,484 @@ +# pragma version 0.3.10 +# pragma optimize gas +# 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/old/xcp_profit_rebalancing/CurveTwocryptoMath.vy b/contracts/old/xcp_profit_rebalancing/CurveTwocryptoMath.vy new file mode 100644 index 00000000..4bc0a644 --- /dev/null +++ b/contracts/old/xcp_profit_rebalancing/CurveTwocryptoMath.vy @@ -0,0 +1,580 @@ +# pragma version 0.3.10 +# pragma optimize gas +# 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.0.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/old/xcp_profit_rebalancing/CurveTwocryptoViews.vy b/contracts/old/xcp_profit_rebalancing/CurveTwocryptoViews.vy new file mode 100644 index 00000000..1a9feed5 --- /dev/null +++ b/contracts/old/xcp_profit_rebalancing/CurveTwocryptoViews.vy @@ -0,0 +1,415 @@ +# pragma version 0.3.10 +# pragma optimize gas +# 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/old/xcp_profit_rebalancing/README.md b/contracts/old/xcp_profit_rebalancing/README.md new file mode 100644 index 00000000..71a21ef5 --- /dev/null +++ b/contracts/old/xcp_profit_rebalancing/README.md @@ -0,0 +1,3 @@ +# Xcp profit based rebalancing +This folder contains the latest version of the contracts that used to rebalance the pool using xcp profit. +**Commit:** `9856e82f0314654a5740b46ab50181ac137e788b` diff --git a/scripts/experiments/rebalance_playground.py b/scripts/experiments/rebalance_playground.py new file mode 100644 index 00000000..27cda277 --- /dev/null +++ b/scripts/experiments/rebalance_playground.py @@ -0,0 +1,252 @@ +import boa +import matplotlib.pyplot as plt +import pandas as pd +from utils.pool_presets import get_preset_by_name +from utils.tokens import mint_for_testing + +# D profit rebalancing +from contracts.main import CurveTwocrypto as Twocrypto +from contracts.main import CurveTwocryptoFactory as Factory +from contracts.main import CurveTwocryptoMath as Math +from contracts.main import CurveTwocryptoViews as Views +from contracts.mocks import ERC20Mock as ERC20 + +# xcp profit rebalancing +from contracts.old.xcp_profit_rebalancing import CurveTwocrypto as TwocryptoXcp +from contracts.old.xcp_profit_rebalancing import ( + CurveTwocryptoFactory as FactoryXcp, +) +from contracts.old.xcp_profit_rebalancing import CurveTwocryptoMath as MathXcp +from contracts.old.xcp_profit_rebalancing import ( + CurveTwocryptoViews as ViewsXcp, +) + +deployer = boa.env.generate_address() +owner = boa.env.generate_address() +fee_receiver = boa.env.generate_address() + +# deploying contracts +with boa.env.prank(deployer): + amm_implementation = Twocrypto.deploy_as_blueprint() + amm_xcp_implementation = TwocryptoXcp.deploy_as_blueprint() + + math_contract = Math() + math_xcp_contract = MathXcp() + + views_contract = Views() + views_xcp_contract = ViewsXcp() + + factory = Factory() + factory_xcp = FactoryXcp() + factory.initialise_ownership(fee_receiver, owner) + factory_xcp.initialise_ownership(fee_receiver, owner) + +# linking contracts +with boa.env.prank(owner): + factory.set_pool_implementation(amm_implementation, 0) + factory_xcp.set_pool_implementation(amm_xcp_implementation, 0) + factory.set_views_implementation(views_contract) + factory_xcp.set_views_implementation(views_contract) + factory.set_math_implementation(math_contract) + factory_xcp.set_math_implementation(math_contract) + +params = get_preset_by_name("crypto") + +coins = [] +with boa.env.prank(deployer): + coins.append(ERC20("BTC", "BTC", 18)) + coins.append(ERC20("ETH", "ETH", 18)) + +with boa.env.prank(deployer): + swap = factory.deploy_pool( + "new_pool", # _name: String[64] + "BTC<>ETH", # _symbol: String[32] + [coin.address for coin in coins], # _coins: address[N_COINS] + 0, # implementation_id: uint256 + params["A"], # A: uint256 + params["gamma"], # gamma: uint256 + params["mid_fee"], # mid_fee: uint256 + params["out_fee"], # out_fee: uint256 + params["fee_gamma"], # fee_gamma: uint256 + params["allowed_extra_profit"], # allowed_extra_profit: uint256 + params["adjustment_step"], # adjustment_step: uint256 + params["ma_exp_time"], # ma_exp_time: uint256 + int(1e18), # initial_price: uint256 + ) + + pool = Twocrypto.at(swap) + + swap = factory_xcp.deploy_pool( + "old_pool", # _name: String[64] + "BTC<>ETH", # _symbol: String[32] + [coin.address for coin in coins], # _coins: address[N_COINS] + 0, # implementation_id: uint256 + params["A"], # A: uint256 + params["gamma"], # gamma: uint256 + params["mid_fee"], # mid_fee: uint256 + params["out_fee"], # out_fee: uint256 + params["fee_gamma"], # fee_gamma: uint256 + params["allowed_extra_profit"], # allowed_extra_profit: uint256 + params["adjustment_step"], # adjustment_step: uint256 + params["ma_exp_time"], # ma_exp_time: uint256 + int(1e18), + ) + + pool_xcp = TwocryptoXcp.at(swap) + +pools = [pool, pool_xcp] + +# create an empty dataframe +df_pool = pd.DataFrame(columns=["time", "D", "D_rebalance"]) +df_pool_xcp = pd.DataFrame(columns=["time", "D", "D_rebalance"]) + + +def take_snapshot(func_name): + global df_pool, df_pool_xcp, pools + xcpx = (pool.xcp_profit() / 2e18 + pool.xcp_profit_a() / 2e18 + 1) / 2 + xcpx_xcp = ( + (pool_xcp.xcp_profit() / 2e18 + pool_xcp.xcp_profit_a() / 2e18 + 1) + / 2, + ) + + data_pool = pd.DataFrame( + { + "time": boa.env.evm.patch.timestamp, + "D": pool.D(), + "D_rebalance": pool.D_rebalance(), + "xcp": pool.xcp_profit() / 1e18, + "xcpx": xcpx, + "op": func_name, + }, + index=[0], + ) + data_pool_xcp = pd.DataFrame( + { + "time": boa.env.evm.patch.timestamp, + "D": pool_xcp.D(), + "D_rebalance": 0, + "xcp": pool_xcp.xcp_profit() / 1e18, + "xcpx": xcpx_xcp, + "op": func_name, + }, + index=[0], + ) + # convert to df + + df_pool = pd.concat([df_pool, data_pool]) + df_pool_xcp = pd.concat([df_pool_xcp, data_pool_xcp]) + + +def snapshot(function): + def wrapped(*args, delay=60, **kwargs): + boa.env.time_travel(seconds=delay) + function(*args, **kwargs) + take_snapshot(function.__name__) + + return wrapped + + +trader = boa.env.generate_address("trader") + + +@snapshot +def add_liq(amounts, sender=trader): + amounts = [int(a) for a in amounts] + + for p in pools: + for coin, amount in zip(coins, amounts): + # infinite approval + coin.approve(p, 2**256 - 1, sender=sender) + # mint the amount of tokens for the depositor + mint_for_testing(coin, sender, amount) + + p.add_liquidity(amounts, 0, sender=sender) + + +@snapshot +def exchange(amount, i, sender=trader): + amount = int(amount) + + for p in pools: + coins[i].approve(p, 2**256 - 1, sender=sender) + mint_for_testing(coins[i], sender, amount) + p.exchange(i, 1 - i, amount, 0, sender=sender) + + +@snapshot +def donate(amounts): + amounts = [int(a) for a in amounts] + + donor = boa.env.generate_address() + for coin, amount in zip(coins, amounts): + # mint the amount of tokens for the donor + mint_for_testing(coin, donor, amount) + coin.transfer(pool, amount, sender=donor) + pool.donate(amounts, sender=donor) + + +@snapshot +def rem_liq(percentage, sender=trader): + amount = int(pool.balanceOf(sender) * percentage) + + for p in pools: + p.remove_liquidity(amount, [0] * 2, sender=sender) + + +@snapshot +def rem_liq_one(percentage, i, sender=trader): + amount = int(pool.balanceOf(sender) * percentage) + + for p in pools: + p.remove_liquidity_one_coin(amount, i, 0, sender=sender) + + +def plot_cumulative(df): + df["time"] = pd.to_datetime(df["time"], unit="s") + + D = df["D"].astype(float) + D_rebalance = df["D_rebalance"].astype(float) + xcpx = df["xcpx"].astype(float) + xcp = df["xcp"].astype(float) + + # Stack the values + stacked_D_rebalance = D + D_rebalance + + fig, ax1 = plt.subplots() + + # Plotting the stacked values + ax1.fill_between(df["time"], D, label="D", alpha=0.6) + ax1.fill_between( + df["time"], D, stacked_D_rebalance, label="D_rebalance", alpha=0.6 + ) + ax1.set_xlabel("Time") + ax1.set_ylabel("Cumulative Value", color="b") + ax1.legend(loc="lower right") + + # Secondary y-axis + ax2 = ax1.twinx() + ax2.plot(df["time"], xcpx, label="xcpx", color="r") + ax2.plot(df["time"], xcp, label="xcp", color="g") + ax2.set_ylabel("xcpx and xcp Values", color="r") + ax2.legend(loc="right") + + plt.title("Composition of D with profit overlay") + + plt.show() + + print(df[["time", "D", "D_rebalance"]]) + + +if __name__ == "__main__": + add_liq([1e20] * 2) + add_liq([1e20] * 2) + add_liq([1e20] * 2) + add_liq([1e20] * 2) + rem_liq_one(0.1, 0) + # rem_liq(0.1) + # for i in range(3): + # exchange(1e21, 0) + # exchange(1e18, 1) + + plot_cumulative(df_pool_xcp) + plot_cumulative(df_pool)