diff --git a/contracts/amms/stableswapng/CurveStableSwapFactoryNG.vy b/contracts/amms/stableswapng/CurveStableSwapFactoryNG.vy new file mode 100644 index 0000000..23c55dd --- /dev/null +++ b/contracts/amms/stableswapng/CurveStableSwapFactoryNG.vy @@ -0,0 +1,865 @@ +# pragma version 0.3.10 +# pragma evm-version shanghai +""" +@title CurveStableswapFactoryNG +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2023 - all rights reserved +@notice Permissionless pool deployer and registry +""" + +struct PoolArray: + base_pool: address + implementation: address + liquidity_gauge: address + coins: DynArray[address, MAX_COINS] + decimals: DynArray[uint256, MAX_COINS] + n_coins: uint256 + asset_types: DynArray[uint8, MAX_COINS] + +struct BasePoolArray: + lp_token: address + coins: DynArray[address, MAX_COINS] + decimals: uint256 + n_coins: uint256 + asset_types: DynArray[uint8, MAX_COINS] + + +interface AddressProvider: + def admin() -> address: view + +interface ERC20: + def balanceOf(_addr: address) -> uint256: view + def decimals() -> uint256: view + def totalSupply() -> uint256: view + +interface CurvePool: + def A() -> uint256: view + def fee() -> uint256: view + def admin_fee() -> uint256: view + def balances(i: uint256) -> uint256: view + def admin_balances(i: uint256) -> uint256: view + def get_virtual_price() -> uint256: view + def coins(i: uint256) -> address: view + +interface CurveFactoryMetapool: + def coins(i :uint256) -> address: view + def decimals() -> uint256: view + + +event BasePoolAdded: + base_pool: address + +event PlainPoolDeployed: + coins: DynArray[address, MAX_COINS] + A: uint256 + fee: uint256 + deployer: address + +event MetaPoolDeployed: + coin: address + base_pool: address + A: uint256 + fee: uint256 + deployer: address + +event LiquidityGaugeDeployed: + pool: address + gauge: address + +MAX_COINS: constant(uint256) = 8 + +MAX_FEE: constant(uint256) = 5 * 10 ** 9 +FEE_DENOMINATOR: constant(uint256) = 10 ** 10 + +admin: public(address) +future_admin: public(address) + +asset_types: public(HashMap[uint8, String[20]]) + +pool_list: public(address[4294967296]) # master list of pools +pool_count: public(uint256) # actual length of pool_list +pool_data: HashMap[address, PoolArray] + +base_pool_list: public(address[4294967296]) # list of base pools +base_pool_count: public(uint256) # number of base pools +base_pool_data: public(HashMap[address, BasePoolArray]) + +# asset -> is used in a metapool? +base_pool_assets: public(HashMap[address, bool]) + +# index -> implementation address +pool_implementations: public(HashMap[uint256, address]) +metapool_implementations: public(HashMap[uint256, address]) +math_implementation: public(address) +gauge_implementation: public(address) +views_implementation: public(address) + +# fee receiver for all pools +fee_receiver: public(address) + +# mapping of coins -> pools for trading +# a mapping key is generated for each pair of addresses via +# `bitwise_xor(convert(a, uint256), convert(b, uint256))` +markets: HashMap[uint256, address[4294967296]] +market_counts: HashMap[uint256, uint256] + + +@external +def __init__(_fee_receiver: address, _owner: address): + + self.fee_receiver = _fee_receiver + self.admin = _owner + + self.asset_types[0] = "Standard" + self.asset_types[1] = "Oracle" + self.asset_types[2] = "Rebasing" + self.asset_types[3] = "ERC4626" + + +# <--- 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 get_base_pool(_pool: address) -> address: + """ + @notice Get the base pool for a given factory metapool + @param _pool Metapool address + @return Address of base pool + """ + return self.pool_data[_pool].base_pool + + +@view +@external +def get_n_coins(_pool: address) -> (uint256): + """ + @notice Get the number of coins in a pool + @param _pool Pool address + @return Number of coins + """ + return self.pool_data[_pool].n_coins + + +@view +@external +def get_meta_n_coins(_pool: address) -> (uint256, uint256): + """ + @notice Get the number of coins in a metapool + @param _pool Pool address + @return Number of wrapped coins, number of underlying coins + """ + base_pool: address = self.pool_data[_pool].base_pool + return 2, self.base_pool_data[base_pool].n_coins + 1 + + +@view +@external +def get_coins(_pool: address) -> DynArray[address, MAX_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_underlying_coins(_pool: address) -> DynArray[address, MAX_COINS]: + """ + @notice Get the underlying coins within a pool + @dev Reverts if a pool does not exist or is not a metapool + @param _pool Pool address + @return List of coin addresses + """ + coins: DynArray[address, MAX_COINS] = empty(DynArray[address, MAX_COINS]) + base_pool: address = self.pool_data[_pool].base_pool + assert base_pool != empty(address) # dev: pool is not metapool + + coins.append(self.pool_data[_pool].coins[0]) + base_pool_n_coins: uint256 = len(self.base_pool_data[base_pool].coins) + for i in range(1, MAX_COINS): + if i - 1 == base_pool_n_coins: + break + + coins.append(self.base_pool_data[base_pool].coins[i - 1]) + + return coins + + +@view +@external +def get_decimals(_pool: address) -> DynArray[uint256, MAX_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_underlying_decimals(_pool: address) -> DynArray[uint256, MAX_COINS]: + """ + @notice Get decimal places for each underlying coin within a pool + @param _pool Pool address + @return uint256 list of decimals + """ + # decimals are tightly packed as a series of uint8 within a little-endian bytes32 + # the packed value is stored as uint256 to simplify unpacking via shift and modulo + pool_decimals: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + pool_decimals = self.pool_data[_pool].decimals + decimals: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + decimals.append(pool_decimals[0]) + base_pool: address = self.pool_data[_pool].base_pool + packed_decimals: uint256 = self.base_pool_data[base_pool].decimals + + for i in range(MAX_COINS): + unpacked: uint256 = (packed_decimals >> 8 * i) % 256 + if unpacked == 0: + break + + decimals.append(unpacked) + + return decimals + + +@view +@external +def get_metapool_rates(_pool: address) -> DynArray[uint256, MAX_COINS]: + """ + @notice Get rates for coins within a metapool + @param _pool Pool address + @return Rates for each coin, precision normalized to 10**18 + """ + rates: DynArray[uint256, MAX_COINS] = [10**18, 0] + rates[1] = CurvePool(self.pool_data[_pool].base_pool).get_virtual_price() + return rates + + +@view +@external +def get_balances(_pool: address) -> DynArray[uint256, MAX_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 + """ + balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + + if self.pool_data[_pool].base_pool != empty(address): + balances.append(CurvePool(_pool).balances(0)) + balances.append(CurvePool(_pool).balances(1)) + return balances + + n_coins: uint256 = self.pool_data[_pool].n_coins + for i in range(MAX_COINS): + + if i == n_coins: + break + + balances.append(CurvePool(_pool).balances(i)) + + + return balances + + +@view +@external +def get_underlying_balances(_pool: address) -> DynArray[uint256, MAX_COINS]: + """ + @notice Get balances for each underlying coin within a metapool + @param _pool Metapool address + @return uint256 list of underlying balances + """ + + base_pool: address = self.pool_data[_pool].base_pool + assert base_pool != empty(address) # dev: pool is not a metapool + + underlying_balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + underlying_balances[0] = CurvePool(_pool).balances(0) + + base_total_supply: uint256 = ERC20(self.pool_data[_pool].coins[1]).totalSupply() + if base_total_supply > 0: + underlying_pct: uint256 = CurvePool(_pool).balances(1) * 10**36 / base_total_supply + n_coins: uint256 = self.base_pool_data[base_pool].n_coins + for i in range(MAX_COINS): + if i == n_coins: + break + underlying_balances[i + 1] = CurvePool(base_pool).balances(i) * underlying_pct / 10**36 + + return underlying_balances + + +@view +@external +def get_A(_pool: address) -> uint256: + """ + @notice Get the amplfication co-efficient for a pool + @param _pool Pool address + @return uint256 A + """ + return CurvePool(_pool).A() + + +@view +@external +def get_fees(_pool: address) -> (uint256, uint256): + """ + @notice Get the fees for a pool + @dev Fees are expressed as integers + @return Pool fee and admin fee as uint256 with 1e10 precision + """ + return CurvePool(_pool).fee(), CurvePool(_pool).admin_fee() + + +@view +@external +def get_admin_balances(_pool: address) -> DynArray[uint256, MAX_COINS]: + """ + @notice Get the current admin balances (uncollected fees) for a pool + @param _pool Pool address + @return List of uint256 admin balances + """ + n_coins: uint256 = self.pool_data[_pool].n_coins + admin_balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + for i in range(MAX_COINS): + if i == n_coins: + break + admin_balances.append(CurvePool(_pool).admin_balances(i)) + return admin_balances + + +@view +@external +def get_coin_indices( + _pool: address, + _from: address, + _to: address +) -> (int128, int128, bool): + """ + @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 int128 `i`, int128 `j`, boolean indicating if `i` and `j` are underlying coins + """ + coin: address = self.pool_data[_pool].coins[0] + base_pool: address = self.pool_data[_pool].base_pool + if coin in [_from, _to] and base_pool != empty(address): + base_lp_token: address = self.pool_data[_pool].coins[1] + if base_lp_token in [_from, _to]: + # True and False convert to 1 and 0 - a bit of voodoo that + # works because we only ever have 2 non-underlying coins if base pool is empty(address) + return convert(_to == coin, int128), convert(_from == coin, int128), False + + found_market: bool = False + i: uint256 = 0 + j: uint256 = 0 + for x in range(MAX_COINS): + if base_pool == empty(address): + if x >= MAX_COINS: + raise "No available market" + if x != 0: + coin = self.pool_data[_pool].coins[x] + else: + if x != 0: + coin = self.base_pool_data[base_pool].coins[x-1] + if coin == empty(address): + raise "No available market" + if coin == _from: + i = x + elif coin == _to: + j = x + else: + continue + if found_market: + # the second time we find a match, break out of the loop + break + # the first time we find a match, set `found_market` to True + found_market = True + + return convert(i, int128), convert(j, int128), True + + +@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_implementation_address(_pool: address) -> address: + """ + @notice Get the address of the implementation contract used for a factory pool + @param _pool Pool address + @return Implementation contract address + """ + return self.pool_data[_pool].implementation + + +@view +@external +def is_meta(_pool: address) -> bool: + """ + @notice Verify `_pool` is a metapool + @param _pool Pool address + @return True if `_pool` is a metapool + """ + return self.pool_data[_pool].base_pool != empty(address) + + +@view +@external +def get_pool_asset_types(_pool: address) -> DynArray[uint8, MAX_COINS]: + """ + @notice Query the asset type of `_pool` + @param _pool Pool Address + @return Dynarray of uint8 indicating the pool asset type + Asset Types: + 0. Standard ERC20 token with no additional features + 1. Oracle - token with rate oracle (e.g. wrapped staked ETH) + 2. Rebasing - token with rebase (e.g. staked ETH) + 3. ERC4626 - e.g. sDAI + """ + return self.pool_data[_pool].asset_types + + +# <--- Pool Deployers ---> + +@external +def deploy_plain_pool( + _name: String[32], + _symbol: String[10], + _coins: DynArray[address, MAX_COINS], + _A: uint256, + _fee: uint256, + _offpeg_fee_multiplier: uint256, + _ma_exp_time: uint256, + _implementation_idx: uint256, + _asset_types: DynArray[uint8, MAX_COINS], + _method_ids: DynArray[bytes4, MAX_COINS], + _oracles: DynArray[address, MAX_COINS], +) -> address: + """ + @notice Deploy a new plain pool + @param _name Name of the new plain pool + @param _symbol Symbol for the new plain pool - will be + concatenated with factory symbol + @param _coins List of addresses of the coins being used in the pool. + @param _A Amplification co-efficient - a lower value here means + less tolerance for imbalance within the pool's assets. + Suggested values include: + * Uncollateralized algorithmic stablecoins: 5-10 + * Non-redeemable, collateralized assets: 100 + * Redeemable assets: 200-400 + @param _fee Trade fee, given as an integer with 1e10 precision. The + maximum is 1% (100000000). 50% of the fee is distributed to veCRV holders. + @param _ma_exp_time Averaging window of oracle. Set as time_in_seconds / ln(2) + Example: for 10 minute EMA, _ma_exp_time is 600 / ln(2) ~= 866 + @param _implementation_idx Index of the implementation to use + @param _asset_types Asset types for pool, as an integer + @param _method_ids Array of first four bytes of the Keccak-256 hash of the function signatures + of the oracle addresses that gives rate oracles. + Calculated as: keccak(text=event_signature.replace(" ", ""))[:4] + @param _oracles Array of rate oracle addresses. + @return Address of the deployed pool + """ + assert len(_coins) >= 2 # dev: pool needs to have at least two coins! + assert len(_coins) == len(_method_ids) # dev: All coin arrays should be same length + assert len(_coins) == len(_oracles) # dev: All coin arrays should be same length + assert len(_coins) == len(_asset_types) # dev: All coin arrays should be same length + assert _fee <= 100000000, "Invalid fee" + assert _offpeg_fee_multiplier * _fee <= MAX_FEE * FEE_DENOMINATOR + + n_coins: uint256 = len(_coins) + _rate_multipliers: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + decimals: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + + for i in range(MAX_COINS): + if i == n_coins: + break + + coin: address = _coins[i] + + decimals.append(ERC20(coin).decimals()) + assert decimals[i] < 19, "Max 18 decimals for coins" + + _rate_multipliers.append(10 ** (36 - decimals[i])) + + for j in range(i, i + MAX_COINS): + if (j + 1) == n_coins: + break + assert coin != _coins[j+1], "Duplicate coins" + + implementation: address = self.pool_implementations[_implementation_idx] + assert implementation != empty(address), "Invalid implementation index" + + pool: address = create_from_blueprint( + implementation, + _name, # _name: String[32] + _symbol, # _symbol: String[10] + _A, # _A: uint256 + _fee, # _fee: uint256 + _offpeg_fee_multiplier, # _offpeg_fee_multiplier: uint256 + _ma_exp_time, # _ma_exp_time: uint256 + _coins, # _coins: DynArray[address, MAX_COINS] + _rate_multipliers, # _rate_multipliers: DynArray[uint256, MAX_COINS] + _asset_types, # _asset_types: DynArray[uint8, MAX_COINS] + _method_ids, # _method_ids: DynArray[bytes4, MAX_COINS] + _oracles, # _oracles: DynArray[address, MAX_COINS] + code_offset=3 + ) + + length: uint256 = self.pool_count + self.pool_list[length] = pool + self.pool_count = length + 1 + self.pool_data[pool].decimals = decimals + self.pool_data[pool].n_coins = n_coins + self.pool_data[pool].base_pool = empty(address) + self.pool_data[pool].implementation = implementation + self.pool_data[pool].asset_types = _asset_types + + for i in range(MAX_COINS): + if i == n_coins: + break + + coin: address = _coins[i] + self.pool_data[pool].coins.append(coin) + + for j in range(i, i + MAX_COINS): + if (j + 1) == n_coins: + break + swappable_coin: address = _coins[j + 1] + key: uint256 = (convert(coin, uint256) ^ convert(swappable_coin, uint256)) + length = self.market_counts[key] + self.markets[key][length] = pool + self.market_counts[key] = length + 1 + + log PlainPoolDeployed(_coins, _A, _fee, msg.sender) + return pool + + +@external +def deploy_metapool( + _base_pool: address, + _name: String[32], + _symbol: String[10], + _coin: address, + _A: uint256, + _fee: uint256, + _offpeg_fee_multiplier: uint256, + _ma_exp_time: uint256, + _implementation_idx: uint256, + _asset_type: uint8, + _method_id: bytes4, + _oracle: address, +) -> address: + """ + @notice Deploy a new metapool + @param _base_pool Address of the base pool to use + within the metapool + @param _name Name of the new metapool + @param _symbol Symbol for the new metapool - will be + concatenated with the base pool symbol + @param _coin Address of the coin being used in the metapool + @param _A Amplification co-efficient - a higher value here means + less tolerance for imbalance within the pool's assets. + Suggested values include: + * Uncollateralized algorithmic stablecoins: 5-10 + * Non-redeemable, collateralized assets: 100 + * Redeemable assets: 200-400 + @param _fee Trade fee, given as an integer with 1e10 precision. The + the maximum is 1% (100000000). + 50% of the fee is distributed to veCRV holders. + @param _ma_exp_time Averaging window of oracle. Set as time_in_seconds / ln(2) + Example: for 10 minute EMA, _ma_exp_time is 600 / ln(2) ~= 866 + @param _implementation_idx Index of the implementation to use + @param _asset_type Asset type for token, as an integer + @param _method_id First four bytes of the Keccak-256 hash of the function signatures + of the oracle addresses that gives rate oracles. + Calculated as: keccak(text=event_signature.replace(" ", ""))[:4] + @param _oracle Rate oracle address. + @return Address of the deployed pool + """ + assert not self.base_pool_assets[_coin], "Invalid asset: Cannot pair base pool asset with base pool's LP token" + assert _fee <= 100000000, "Invalid fee" + assert _offpeg_fee_multiplier * _fee <= MAX_FEE * FEE_DENOMINATOR + + base_pool_n_coins: uint256 = len(self.base_pool_data[_base_pool].coins) + assert base_pool_n_coins != 0, "Base pool is not added" + + implementation: address = self.metapool_implementations[_implementation_idx] + assert implementation != empty(address), "Invalid implementation index" + + # things break if a token has >18 decimals + decimals: uint256 = ERC20(_coin).decimals() + assert decimals < 19, "Max 18 decimals for coins" + + # combine _coins's _asset_type and basepool coins _asset_types: + base_pool_asset_types: DynArray[uint8, MAX_COINS] = self.base_pool_data[_base_pool].asset_types + asset_types: DynArray[uint8, MAX_COINS] = [_asset_type, 0] + + for i in range(0, MAX_COINS): + if i == base_pool_n_coins: + break + asset_types.append(base_pool_asset_types[i]) + + _coins: DynArray[address, MAX_COINS] = [_coin, self.base_pool_data[_base_pool].lp_token] + _rate_multipliers: DynArray[uint256, MAX_COINS] = [10 ** (36 - decimals), 10 ** 18] + _method_ids: DynArray[bytes4, MAX_COINS] = [_method_id, empty(bytes4)] + _oracles: DynArray[address, MAX_COINS] = [_oracle, empty(address)] + + pool: address = create_from_blueprint( + implementation, + _name, # _name: String[32] + _symbol, # _symbol: String[10] + _A, # _A: uint256 + _fee, # _fee: uint256 + _offpeg_fee_multiplier, # _offpeg_fee_multiplier: uint256 + _ma_exp_time, # _ma_exp_time: uint256 + self.math_implementation, # _math_implementation: address + _base_pool, # _base_pool: address + _coins, # _coins: DynArray[address, MAX_COINS] + self.base_pool_data[_base_pool].coins, # base_coins: DynArray[address, MAX_COINS] + _rate_multipliers, # _rate_multipliers: DynArray[uint256, MAX_COINS] + asset_types, # asset_types: DynArray[uint8, MAX_COINS] + _method_ids, # _method_ids: DynArray[bytes4, MAX_COINS] + _oracles, # _oracles: DynArray[address, MAX_COINS] + code_offset=3 + ) + + # add pool to pool_list + length: uint256 = self.pool_count + self.pool_list[length] = pool + self.pool_count = length + 1 + + base_lp_token: address = self.base_pool_data[_base_pool].lp_token + + self.pool_data[pool].decimals = [decimals, 18, 0, 0, 0, 0, 0, 0] + self.pool_data[pool].n_coins = 2 + self.pool_data[pool].base_pool = _base_pool + self.pool_data[pool].coins = [_coin, self.base_pool_data[_base_pool].lp_token] + self.pool_data[pool].implementation = implementation + + is_finished: bool = False + swappable_coin: address = empty(address) + for i in range(MAX_COINS): + if i < len(self.base_pool_data[_base_pool].coins): + swappable_coin = self.base_pool_data[_base_pool].coins[i] + else: + is_finished = True + swappable_coin = base_lp_token + + key: uint256 = (convert(_coin, uint256) ^ convert(swappable_coin, uint256)) + length = self.market_counts[key] + self.markets[key][length] = pool + self.market_counts[key] = length + 1 + + if is_finished: + break + + log MetaPoolDeployed(_coin, _base_pool, _A, _fee, msg.sender) + return 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" + implementation: address = self.gauge_implementation + assert 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 add_base_pool( + _base_pool: address, + _base_lp_token: address, + _asset_types: DynArray[uint8, MAX_COINS], + _n_coins: uint256, +): + """ + @notice Add a base pool to the registry, which may be used in factory metapools + @dev 1. Only callable by admin + 2. Rebasing tokens are not allowed in the base pool. + 3. Do not add base pool which contains native tokens (e.g. ETH). + 4. As much as possible: use standard ERC20 tokens. + Should you choose to deviate from these recommendations, audits are advised. + @param _base_pool Pool address to add + @param _asset_types Asset type for pool, as an integer + """ + assert msg.sender == self.admin # dev: admin-only function + assert 2 not in _asset_types # dev: rebasing tokens cannot be in base pool + assert len(self.base_pool_data[_base_pool].coins) == 0 # dev: pool exists + assert _n_coins < MAX_COINS # dev: base pool can only have (MAX_COINS - 1) coins. + + # add pool to pool_list + length: uint256 = self.base_pool_count + self.base_pool_list[length] = _base_pool + self.base_pool_count = length + 1 + self.base_pool_data[_base_pool].lp_token = _base_lp_token + self.base_pool_data[_base_pool].n_coins = _n_coins + self.base_pool_data[_base_pool].asset_types = _asset_types + + decimals: uint256 = 0 + coins: DynArray[address, MAX_COINS] = empty(DynArray[address, MAX_COINS]) + coin: address = empty(address) + for i in range(MAX_COINS): + if i == _n_coins: + break + coin = CurvePool(_base_pool).coins(i) + assert coin != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE # dev: native token is not supported + self.base_pool_data[_base_pool].coins.append(coin) + self.base_pool_assets[coin] = True + decimals += (ERC20(coin).decimals() << i*8) + self.base_pool_data[_base_pool].decimals = decimals + + log BasePoolAdded(_base_pool) + + +@external +def set_pool_implementations( + _implementation_index: uint256, + _implementation: address, +): + """ + @notice Set implementation contracts for pools + @dev Only callable by admin + @param _implementation_index Implementation index where implementation is stored + @param _implementation Implementation address to use when deploying plain pools + """ + assert msg.sender == self.admin # dev: admin-only function + self.pool_implementations[_implementation_index] = _implementation + + +@external +def set_metapool_implementations( + _implementation_index: uint256, + _implementation: address, +): + """ + @notice Set implementation contracts for metapools + @dev Only callable by admin + @param _implementation_index Implementation index where implementation is stored + @param _implementation Implementation address to use when deploying meta pools + """ + assert msg.sender == self.admin # dev: admin-only function + self.metapool_implementations[_implementation_index] = _implementation + + +@external +def set_math_implementation(_math_implementation: address): + """ + @notice Set implementation contracts for StableSwap Math + @dev Only callable by admin + @param _math_implementation Address of the math implementation contract + """ + assert msg.sender == self.admin # dev: admin-only function + self.math_implementation = _math_implementation + + +@external +def set_gauge_implementation(_gauge_implementation: address): + """ + @notice Set implementation contracts for liquidity gauge + @dev Only callable by admin + @param _gauge_implementation Address of the gauge blueprint implementation contract + """ + assert msg.sender == self.admin # dev: admin-only function + self.gauge_implementation = _gauge_implementation + + +@external +def set_views_implementation(_views_implementation: address): + """ + @notice Set implementation contracts for Views methods + @dev Only callable by admin + @param _views_implementation Implementation address of views contract + """ + assert msg.sender == self.admin # dev: admin-only function + self.views_implementation = _views_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 + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + self.future_admin = empty(address) + + +@external +def set_fee_receiver(_pool: address, _fee_receiver: address): + """ + @notice Set fee receiver for all pools + @param _pool Address of pool to set fee receiver for. + @param _fee_receiver Address that fees are sent to + """ + assert msg.sender == self.admin # dev: admin only + self.fee_receiver = _fee_receiver + + +@external +def add_asset_type(_id: uint8, _name: String[10]): + """ + @notice Admin only method that adds a new asset type. + @param _id asset type id. + @param _name Name of the asset type. + """ + assert msg.sender == self.admin # dev: admin only + self.asset_types[_id] = _name diff --git a/contracts/amms/stableswapng/CurveStableSwapMetaNG.vy b/contracts/amms/stableswapng/CurveStableSwapMetaNG.vy new file mode 100644 index 0000000..62be99e --- /dev/null +++ b/contracts/amms/stableswapng/CurveStableSwapMetaNG.vy @@ -0,0 +1,1901 @@ +# pragma version 0.3.10 +# pragma optimize codesize +# pragma evm-version paris +""" +@title CurveStableSwapMetaNG +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Stableswap Metapool implementation for 2 coins. Supports pegged assets. +@dev Metapools are pools where the coin on index 1 is a liquidity pool token + of another pool. This exposes methods such as exchange_underlying, which + exchanges token 0 <> token b1, b2, .. bn, where b is base pool and bn is the + nth coin index of the base pool. + CAUTION: Does not work if base pool is an NG pool. Use a different metapool + implementation index in the factory. + Asset Types: + 0. Standard ERC20 token with no additional features. + Note: Users are advised to do careful due-diligence on + ERC20 tokens that they interact with, as this + contract cannot differentiate between harmless and + malicious ERC20 tokens. + 1. Oracle - token with rate oracle (e.g. wstETH) + Note: Oracles may be controlled externally by an EOA. Users + are advised to proceed with caution. + 2. Rebasing - token with rebase (e.g. stETH). + Note: Users and Integrators are advised to understand how + the AMM contract works with rebasing balances. + 3. ERC4626 - token with convertToAssets method (e.g. sDAI). + Note: Some ERC4626 implementations may be susceptible to + Donation/Inflation attacks. Users are advised to + proceed with caution. + NOTE: Pool Cannot support tokens with multiple asset types: e.g. ERC4626 + with fees are not supported. + Supports: + 1. ERC20 support for return True/revert, return True/False, return None + 2. ERC20 tokens can have arbitrary decimals (<=18). + 3. ERC20 tokens that rebase (either positive or fee on transfer) + 4. ERC20 tokens that have a rate oracle (e.g. wstETH, cbETH, sDAI, etc.) + Note: Oracle precision _must_ be 10**18. + 5. ERC4626 tokens with arbitrary precision (<=18) of Vault token and underlying + asset. + Additional features include: + 1. Adds oracles based on AMM State Price (and _not_ last traded price). + State prices are calculated _after_ liquidity operations, using bonding + curve math. + 2. Adds an exponential moving average oracle for D. + 3. `exchange_received`: swaps that expect an ERC20 transfer to have occurred + prior to executing the swap. + Note: a. If pool contains rebasing tokens and one of the `asset_types` is 2 (Rebasing) + then calling `exchange_received` will REVERT. + b. If pool contains rebasing token and `asset_types` does not contain 2 (Rebasing) + then this is an incorrect implementation and rebases can be + stolen. + 4. Adds `get_dx`, `get_dx_underlying`: Similar to `get_dy` which returns an expected output + of coin[j] for given `dx` amount of coin[i], `get_dx` returns expected + input of coin[i] for an output amount of coin[j]. + 5. Fees are dynamic: AMM will charge a higher fee if pool depegs. This can cause very + slight discrepancies between calculated fees and realised fees. +""" + +from vyper.interfaces import ERC20 +from vyper.interfaces import ERC20Detailed +from vyper.interfaces import ERC4626 + +implements: ERC20 + +# ------------------------------- Interfaces --------------------------------- + +interface Factory: + def fee_receiver() -> address: view + def admin() -> address: view + def views_implementation() -> address: view + +interface ERC1271: + def isValidSignature(_hash: bytes32, _signature: Bytes[65]) -> bytes32: view + +interface StableSwapViews: + def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view + def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view + def get_dx_underlying(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view + def get_dy_underlying(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view + def dynamic_fee(i: int128, j: int128, pool: address) -> uint256: view + def calc_token_amount( + _amounts: DynArray[uint256, MAX_COINS], + _is_deposit: bool, + _pool: address + ) -> uint256: view + +interface StableSwap2: + def add_liquidity(amounts: uint256[2], min_mint_amount: uint256): nonpayable + +interface StableSwap3: + def add_liquidity(amounts: uint256[3], min_mint_amount: uint256): nonpayable + +interface StableSwapNG: + def add_liquidity( + amounts: DynArray[uint256, MAX_COINS], + min_mint_amount: uint256 + ) -> uint256: nonpayable + +interface StableSwap: + def remove_liquidity_one_coin(_token_amount: uint256, i: int128, min_amount: uint256): nonpayable + def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256): nonpayable + def get_virtual_price() -> uint256: view + +interface Math: + def get_y( + i: int128, + j: int128, + x: uint256, + xp: DynArray[uint256, MAX_COINS], + _amp: uint256, + _D: uint256, + _n_coins: uint256 + ) -> uint256: view + def get_y_D( + A: uint256, + i: int128, + xp: DynArray[uint256, MAX_COINS], + D: uint256, + _n_coins: uint256 + ) -> uint256: view + def get_D( + _xp: DynArray[uint256, MAX_COINS], + _amp: uint256, + _n_coins: uint256 + ) -> uint256: view + def exp(x: int256) -> 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: int128 + tokens_sold: uint256 + bought_id: int128 + tokens_bought: uint256 + +event TokenExchangeUnderlying: + buyer: indexed(address) + sold_id: int128 + tokens_sold: uint256 + bought_id: int128 + tokens_bought: uint256 + +event AddLiquidity: + provider: indexed(address) + token_amounts: DynArray[uint256, MAX_COINS] + fees: DynArray[uint256, MAX_COINS] + invariant: uint256 + token_supply: uint256 + +event RemoveLiquidity: + provider: indexed(address) + token_amounts: DynArray[uint256, MAX_COINS] + fees: DynArray[uint256, MAX_COINS] + token_supply: uint256 + +event RemoveLiquidityOne: + provider: indexed(address) + token_id: int128 + token_amount: uint256 + coin_amount: uint256 + token_supply: uint256 + +event RemoveLiquidityImbalance: + provider: indexed(address) + token_amounts: DynArray[uint256, MAX_COINS] + fees: DynArray[uint256, MAX_COINS] + invariant: uint256 + token_supply: uint256 + +event RampA: + old_A: uint256 + new_A: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + A: uint256 + t: uint256 + +event ApplyNewFee: + fee: uint256 + offpeg_fee_multiplier: uint256 + +event SetNewMATime: + ma_exp_time: uint256 + D_ma_time: uint256 + + +MAX_COINS: constant(uint256) = 8 # max coins is 8 in the factory +MAX_COINS_128: constant(int128) = 8 +MAX_METAPOOL_COIN_INDEX: constant(int128) = 1 + +# ---------------------------- Pool Variables -------------------------------- + +N_COINS: public(constant(uint256)) = 2 +N_COINS_128: constant(int128) = 2 +PRECISION: constant(uint256) = 10 ** 18 + +BASE_POOL: public(immutable(address)) +BASE_POOL_IS_NG: immutable(bool) +BASE_N_COINS: public(immutable(uint256)) +BASE_COINS: public(immutable(DynArray[address, MAX_COINS])) + +math: immutable(Math) +factory: immutable(Factory) +coins: public(immutable(DynArray[address, MAX_COINS])) +asset_type: immutable(uint8) +pool_contains_rebasing_tokens: immutable(bool) +stored_balances: uint256[N_COINS] + +# Fee specific vars +FEE_DENOMINATOR: constant(uint256) = 10 ** 10 +fee: public(uint256) # fee * 1e10 +offpeg_fee_multiplier: public(uint256) # * 1e10 +admin_fee: public(constant(uint256)) = 5000000000 +MAX_FEE: constant(uint256) = 5 * 10 ** 9 + +# ---------------------- Pool Amplification Parameters ----------------------- + +A_PRECISION: constant(uint256) = 100 +MAX_A: constant(uint256) = 10 ** 6 +MAX_A_CHANGE: constant(uint256) = 10 + +initial_A: public(uint256) +future_A: public(uint256) +initial_A_time: public(uint256) +future_A_time: public(uint256) + +# ---------------------------- Admin Variables ------------------------------- + +MIN_RAMP_TIME: constant(uint256) = 86400 +admin_balances: public(DynArray[uint256, MAX_COINS]) + +# ----------------------- Oracle Specific vars ------------------------------- + +rate_multiplier: immutable(uint256) +# [bytes4 method_id][bytes8 ][bytes20 oracle] +rate_oracle: immutable(uint256) # this is the rate oracle for the token at 0th index + +# For ERC4626 tokens, we need: +call_amount: immutable(uint256) +scale_factor: immutable(uint256) + +last_prices_packed: uint256 # packing: last_price, ma_price +last_D_packed: uint256 # packing: last_D, ma_D +ma_exp_time: public(uint256) +D_ma_time: public(uint256) +ma_last_time: public(uint256) # packing: ma_last_time_p, ma_last_time_D + +# shift(2**32 - 1, 224) +ORACLE_BIT_MASK: constant(uint256) = (2**32 - 1) * 256**28 + +# --------------------------- ERC20 Specific Vars ---------------------------- + +name: public(immutable(String[64])) +symbol: public(immutable(String[32])) +decimals: public(constant(uint8)) = 18 +version: public(constant(String[8])) = "v7.0.0" + +balanceOf: public(HashMap[address, uint256]) +allowance: public(HashMap[address, HashMap[address, uint256]]) +total_supply: uint256 +nonces: public(HashMap[address, uint256]) + +# keccak256("isValidSignature(bytes32,bytes)")[:4] << 224 +ERC1271_MAGIC_VAL: constant(bytes32) = 0x1626ba7e00000000000000000000000000000000000000000000000000000000 +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) + + +# ------------------------------ AMM Setup ----------------------------------- + + +@external +def __init__( + _name: String[32], + _symbol: String[10], + _A: uint256, + _fee: uint256, + _offpeg_fee_multiplier: uint256, + _ma_exp_time: uint256, + _math_implementation: address, + _base_pool: address, + _coins: DynArray[address, MAX_COINS], + _base_coins: DynArray[address, MAX_COINS], + _rate_multipliers: DynArray[uint256, MAX_COINS], + _asset_types: DynArray[uint8, MAX_COINS], + _method_ids: DynArray[bytes4, MAX_COINS], + _oracles: DynArray[address, MAX_COINS], +): + """ + @notice Initialize the pool contract + @param _name Name of the new plain pool. + @param _symbol Symbol for the new plain pool. + @param _A Amplification co-efficient - a lower value here means + less tolerance for imbalance within the pool's assets. + Suggested values include: + * Uncollateralized algorithmic stablecoins: 5-10 + * Non-redeemable, collateralized assets: 100 + * Redeemable assets: 200-400 + @param _fee Trade fee, given as an integer with 1e10 precision. The + the maximum is 1% (100000000). + 50% of the fee is distributed to veCRV holders. + @param _offpeg_fee_multiplier A multiplier that determines how much to increase + Fees by when assets in the AMM depeg. Example: 20000000000 + @param _ma_exp_time Averaging window of oracle. Set as time_in_seconds / ln(2) + Example: for 10 minute EMA, _ma_exp_time is 600 / ln(2) ~= 866 + @param _math_implementation Contract containing Math methods + @param _base_pool The underlying AMM of the LP token _coins[0] is paired against + @param _coins List of addresses of the coins being used in the pool. For metapool this is + the coin (say LUSD) vs (say) 3crv as: [LUSD, 3CRV]. Length is always 2. + @param _base_coins coins in the underlying base pool. + @param _rate_multipliers Rate multipliers of the individual coins. For Metapools it is: + [10 ** (36 - _coins[0].decimals()), 10 ** 18]. + @param _asset_types Array of uint8 representing tokens in pool + @param _method_ids Array of first four bytes of the Keccak-256 hash of the function signatures + of the oracle addresses that gives rate oracles. + Calculated as: keccak(text=event_signature.replace(" ", ""))[:4] + @param _oracles Array of rate oracle addresses. + """ + # The following reverts if BASE_POOL is an NG implementaion. + BASE_POOL_IS_NG = raw_call(_base_pool, method_id("D_ma_time()"), revert_on_failure=False) + + if not BASE_POOL_IS_NG: + assert len(_base_coins) <= 3 # dev: implementation does not support old gen base pool with more than 3 coins + + math = Math(_math_implementation) + BASE_POOL = _base_pool + BASE_COINS = _base_coins + BASE_N_COINS = len(_base_coins) + coins = _coins # <---------------- coins[1] is always base pool LP token. + + asset_type = _asset_types[0] + pool_contains_rebasing_tokens = asset_type == 2 + rate_multiplier = _rate_multipliers[0] + + for i in range(MAX_COINS): + if i < BASE_N_COINS: + # Approval needed for add_liquidity operation on base pool in + # _exchange_underlying: + assert ERC20(_base_coins[i]).approve( + BASE_POOL, + max_value(uint256), + default_return_value = True + ) + + # For ERC4626 tokens: + if asset_type == 3: + # In Vyper 0.3.10, if immutables are not set, because of an if-statement, + # it is by default set to 0; this is fine in the case of these two + # immutables, since they are only used if asset_types[0] == 3. + call_amount = 10**convert(ERC20Detailed(_coins[0]).decimals(), uint256) + scale_factor = 10**(18 - convert(ERC20Detailed(ERC4626(_coins[0]).asset()).decimals(), uint256)) + + # ----------------- Parameters independent of pool type ------------------ + + factory = Factory(msg.sender) + + A: uint256 = unsafe_mul(_A, A_PRECISION) + self.initial_A = A + self.future_A = A + self.fee = _fee + self.offpeg_fee_multiplier = _offpeg_fee_multiplier + + assert _ma_exp_time != 0 + self.ma_exp_time = _ma_exp_time + self.D_ma_time = 62324 # <--------- 12 hours default on contract start. + self.ma_last_time = self.pack_2(block.timestamp, block.timestamp) + + self.last_prices_packed = self.pack_2(10**18, 10**18) + self.admin_balances = [0, 0] + self.stored_balances = [0, 0] + + rate_oracle = convert(_method_ids[0], uint256) * 2**224 | convert(_oracles[0], uint256) + + # --------------------------- ERC20 stuff ---------------------------- + + name = _name + symbol = _symbol + + # EIP712 related params ----------------- + NAME_HASH = keccak256(name) + salt = block.prevhash + CACHED_CHAIN_ID = chain.id + CACHED_DOMAIN_SEPARATOR = keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + + # ------------------------ Fire a transfer event ------------------------- + + log Transfer(empty(address), msg.sender, 0) + + +# ------------------ Token transfers in and out of the AMM ------------------- + + +@internal +def _transfer_in( + coin_metapool_idx: int128, + coin_basepool_idx: int128, + dx: uint256, + sender: address, + expect_optimistic_transfer: bool, + is_base_pool_swap: bool = False, +) -> uint256: + """ + @notice Contains all logic to handle ERC20 token transfers. + @param coin_metapool_idx metapool index of input coin + @param coin_basepool_idx basepool index of input coin + @param dx amount of `_coin` to transfer into the pool. + @param sender address to transfer `_coin` from. + @param expect_optimistic_transfer True if contract expects an optimistic coin transfer + @param is_base_pool_swap Default is set to False. + @return amount of coins received + """ + _input_coin: ERC20 = ERC20(coins[coin_metapool_idx]) + _input_coin_is_in_base_pool: bool = False + + # Check if _transfer_in is being called by _exchange_underlying: + if coin_basepool_idx >= 0 and coin_metapool_idx == 1: + + _input_coin = ERC20(BASE_COINS[coin_basepool_idx]) + _input_coin_is_in_base_pool = True + + _dx: uint256 = _input_coin.balanceOf(self) + + # ------------------------- Handle Transfers ----------------------------- + + if expect_optimistic_transfer: + + if not _input_coin_is_in_base_pool: + _dx = _dx - self.stored_balances[coin_metapool_idx] + assert _dx >= dx # dev: pool did not receive tokens for swap + + else: + + assert dx > 0 # dev : do not transferFrom 0 tokens into the pool + assert _input_coin.transferFrom( + sender, + self, + dx, + default_return_value=True + ) + _dx = _input_coin.balanceOf(self) - _dx + + # ------------ Check if liquidity needs to be added somewhere ------------ + + if _input_coin_is_in_base_pool: + if is_base_pool_swap: + return _dx # <----- _exchange_underlying: all input goes to swap. + # So, we will not increment self.stored_balances for metapool_idx. + + # Swap involves base <> meta pool interaction. Add incoming base pool + # token to the base pool, mint _dx base pool LP token (idx 1) and add + # that to self.stored_balances and return that instead. + _dx = self._meta_add_liquidity(_dx, coin_basepool_idx) + + # ----------------------- Update Stored Balances ------------------------- + + self.stored_balances[coin_metapool_idx] += _dx + + return _dx + + +@internal +def _transfer_out( + _coin_idx: int128, _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_coin`, `_exchange`, `_withdraw_admin_fees` and + `remove_liquidity_imbalance` methods. + @param _coin_idx Index of the token to transfer out + @param _amount Amount of token to transfer out + @param receiver Address to send the tokens to + """ + assert receiver != empty(address) # dev: do not send tokens to zero_address + + if not pool_contains_rebasing_tokens: + + # we need not cache balanceOf pool before swap out + self.stored_balances[_coin_idx] -= _amount + assert ERC20(coins[_coin_idx]).transfer( + receiver, _amount, default_return_value=True + ) + + else: + + # cache balances pre and post to account for fee on transfers etc. + coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) + assert ERC20(coins[_coin_idx]).transfer( + receiver, _amount, default_return_value=True + ) + self.stored_balances[_coin_idx] = coin_balance - _amount + + +# -------------------------- AMM Special Methods ----------------------------- + + +@view +@internal +def _stored_rates() -> uint256[N_COINS]: + """ + @notice Gets rate multipliers for each coin. + @dev If the coin has a rate oracle that has been properly initialised, + this method queries that rate by static-calling an external + contract. + """ + rates: uint256[N_COINS] = [rate_multiplier, StableSwap(BASE_POOL).get_virtual_price()] + + if asset_type == 1 and not rate_oracle == 0: + + # NOTE: fetched_rate is assumed to be 10**18 precision + oracle_response: Bytes[32] = raw_call( + convert(rate_oracle % 2**160, address), + _abi_encode(rate_oracle & ORACLE_BIT_MASK), + max_outsize=32, + is_static_call=True, + ) + assert len(oracle_response) == 32 + fetched_rate: uint256 = convert(oracle_response, uint256) + + # rates[0] * fetched_rate / PRECISION + rates[0] = unsafe_div(rates[0] * fetched_rate, PRECISION) + + elif asset_type == 3: # ERC4626 + + # rates[0] * fetched_rate / PRECISION + rates[0] = unsafe_div( + rates[0] * ERC4626(coins[0]).convertToAssets(call_amount) * scale_factor, + PRECISION + ) # 1e18 precision + + return rates + + +@view +@internal +def _balances() -> uint256[N_COINS]: + """ + @notice Calculates the pool's balances _excluding_ the admin's balances. + @dev If the pool contains rebasing tokens, this method ensures LPs keep all + rebases and admin only claims swap fees. This also means that, since + admin's balances are stored in an array and not inferred from read balances, + the fees in the rebasing token that the admin collects is immune to + slashing events. + """ + result: uint256[N_COINS] = empty(uint256[N_COINS]) + admin_balances: DynArray[uint256, MAX_COINS] = self.admin_balances + for i in range(N_COINS_128): + + if pool_contains_rebasing_tokens: + # Read balances by gulping to account for rebases + result[i] = ERC20(coins[i]).balanceOf(self) - admin_balances[i] + else: + # Use cached balances + result[i] = self.stored_balances[i] - admin_balances[i] + + return result + + +# -------------------------- AMM Main Functions ------------------------------ + + +@external +@nonreentrant('lock') +def exchange( + i: int128, + j: int128, + _dx: uint256, + _min_dy: uint256, + _receiver: address = msg.sender, +) -> uint256: + """ + @notice Perform an exchange between two coins + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param _dx Amount of `i` being exchanged + @param _min_dy Minimum amount of `j` to receive + @param _receiver Address that receives `j` + @return Actual amount of `j` received + """ + return self._exchange( + msg.sender, + i, + j, + _dx, + _min_dy, + _receiver, + False + ) + + +@external +@nonreentrant('lock') +def exchange_received( + i: int128, + j: int128, + _dx: uint256, + _min_dy: uint256, + _receiver: address = msg.sender, +) -> uint256: + """ + @notice Perform an exchange between two coins without transferring token in + @dev The contract swaps tokens based on a change in balance of coin[i]. The + dx = ERC20(coin[i]).balanceOf(self) - self.stored_balances[i]. Users of + this method are dex aggregators, arbitrageurs, or other users who do not + wish to grant approvals to the contract: they would instead send tokens + directly to the contract and call `exchange_received`. + Note: This is disabled if pool contains rebasing tokens. + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param _dx Amount of `i` being exchanged + @param _min_dy Minimum amount of `j` to receive + @param _receiver Address that receives `j` + @return Actual amount of `j` received + """ + assert not pool_contains_rebasing_tokens # dev: exchange_received not supported if pool contains rebasing tokens + return self._exchange( + msg.sender, + i, + j, + _dx, + _min_dy, + _receiver, + True, # <--------------------------------------- swap optimistically. + ) + + +@external +@nonreentrant('lock') +def exchange_underlying( + i: int128, + j: int128, + _dx: uint256, + _min_dy: uint256, + _receiver: address = msg.sender, +) -> uint256: + """ + @notice Perform an exchange between two underlying coins + @param i Index value for the underlying coin to send + @param j Index value of the underlying coin to receive + @param _dx Amount of `i` being exchanged + @param _min_dy Minimum amount of `j` to receive + @param _receiver Address that receives `j` + @return Actual amount of `j` received + """ + assert _receiver != empty(address) # dev: do not send tokens to zero_address + + rates: uint256[N_COINS] = self._stored_rates() + old_balances: uint256[N_COINS] = self._balances() + xp: uint256[N_COINS] = self._xp_mem(rates, old_balances) + + dy: uint256 = 0 + base_i: int128 = 0 + base_j: int128 = 0 + meta_i: int128 = 0 + meta_j: int128 = 0 + x: uint256 = 0 + output_coin: address = empty(address) + + # ------------------------ Determine coin indices ------------------------ + + # Get input coin indices: + if i > 0: + base_i = i - MAX_METAPOOL_COIN_INDEX + meta_i = 1 + + # Get output coin and indices: + if j == 0: + output_coin = coins[0] + else: + base_j = j - MAX_METAPOOL_COIN_INDEX + meta_j = 1 + output_coin = BASE_COINS[base_j] + + # --------------------------- Do Transfer in ----------------------------- + + # If incoming coin is supposed to go to the base pool, the _transfer_in + # method will add_liquidity in the base pool and return dx_w_fee LP tokens + dx_w_fee: uint256 = self._transfer_in( + meta_i, + base_i, + _dx, + msg.sender, + False, + (i > 0 and j > 0), # <--- if True: do not add liquidity to base pool. + ) + + # ------------------------------- Exchange ------------------------------- + + if i == 0 or j == 0: # meta swap + + x = xp[meta_i] + unsafe_div(dx_w_fee * rates[meta_i], PRECISION) + dy = self.__exchange(x, xp, rates, meta_i, meta_j) + + # Adjust stored balances of meta-level tokens: + self.stored_balances[meta_j] -= dy + + # Withdraw from the base pool if needed + if j > 0: + out_amount: uint256 = ERC20(output_coin).balanceOf(self) + StableSwap(BASE_POOL).remove_liquidity_one_coin(dy, base_j, 0) + dy = ERC20(output_coin).balanceOf(self) - out_amount + + assert dy >= _min_dy + + else: # base pool swap (user should swap at base pool for better gas) + + dy = ERC20(output_coin).balanceOf(self) + StableSwap(BASE_POOL).exchange(base_i, base_j, dx_w_fee, _min_dy) + dy = ERC20(output_coin).balanceOf(self) - dy + + # --------------------------- Do Transfer out ---------------------------- + + assert ERC20(output_coin).transfer(_receiver, dy, default_return_value=True) + + # ------------------------------------------------------------------------ + + log TokenExchangeUnderlying(msg.sender, i, _dx, j, dy) + + return dy + + +@external +@nonreentrant('lock') +def add_liquidity( + _amounts: uint256[N_COINS], + _min_mint_amount: uint256, + _receiver: address = msg.sender +) -> uint256: + """ + @notice Deposit coins into the pool + @param _amounts List of amounts of coins to deposit + @param _min_mint_amount Minimum amount of LP tokens to mint from the deposit + @param _receiver Address that owns the minted LP tokens + @return Amount of LP tokens received by depositing + """ + assert _receiver != empty(address) # dev: do not send LP tokens to zero_address + + amp: uint256 = self._A() + old_balances: uint256[N_COINS] = self._balances() + rates: uint256[N_COINS] = self._stored_rates() + + # Initial invariant + D0: uint256 = self.get_D_mem(rates, old_balances, amp) + + total_supply: uint256 = self.total_supply + new_balances: uint256[N_COINS] = old_balances + + # -------------------------- Do Transfers In ----------------------------- + + for i in range(N_COINS_128): + + if _amounts[i] > 0: + + new_balances[i] += self._transfer_in( + i, + -1, # <--- we're not handling underlying coins here + _amounts[i], + msg.sender, + False, # expect_optimistic_transfer + ) + + else: + + assert total_supply != 0 # dev: initial deposit requires all coins + + # ------------------------------------------------------------------------ + + # Invariant after change + D1: uint256 = self.get_D_mem(rates, new_balances, amp) + assert D1 > D0 + + # We need to recalculate the invariant accounting for fees + # to calculate fair user's share + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + mint_amount: uint256 = 0 + + if total_supply > 0: + + ideal_balance: uint256 = 0 + difference: uint256 = 0 + new_balance: uint256 = 0 + + ys: uint256 = unsafe_div(D0 + D1, N_COINS) + xs: uint256 = 0 + _dynamic_fee_i: uint256 = 0 + + # Only account for fees if we are not the first to deposit + # base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + # unsafe math is safu here: + base_fee: uint256 = unsafe_div(unsafe_mul(self.fee, N_COINS), 4) + + for i in range(N_COINS_128): + + ideal_balance = D1 * old_balances[i] / D0 + new_balance = new_balances[i] + + # unsafe math is safu here: + if ideal_balance > new_balance: + difference = unsafe_sub(ideal_balance, new_balance) + else: + difference = unsafe_sub(new_balance, ideal_balance) + + # fee[i] = _dynamic_fee(i, j) * difference / FEE_DENOMINATOR + xs = unsafe_div(rates[i] * (old_balances[i] + new_balance), PRECISION) + _dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee) + fees[i] = unsafe_div(_dynamic_fee_i * difference, FEE_DENOMINATOR) + + # fees[i] * admin_fee / FEE_DENOMINATOR + self.admin_balances[i] += unsafe_div(fees[i] * admin_fee, FEE_DENOMINATOR) + new_balances[i] -= fees[i] + + xp: uint256[N_COINS] = self._xp_mem(rates, new_balances) + D1 = math.get_D([xp[0], xp[1]], amp, N_COINS) # <------ Reuse D1 for new D value. + # we do unsafe div here because we already did several safedivs with D0 + mint_amount = unsafe_div(total_supply * (D1 - D0), D0) + self.upkeep_oracles(xp, amp, D1) + + else: + + mint_amount = D1 # Take the dust if there was any + + # (re)instantiate D oracle if totalSupply is zero. + self.last_D_packed = self.pack_2(D1, D1) + + # Update D ma time: + ma_last_time_unpacked: uint256[2] = self.unpack_2(self.ma_last_time) + if ma_last_time_unpacked[1] < block.timestamp: + ma_last_time_unpacked[1] = block.timestamp + self.ma_last_time = self.pack_2(ma_last_time_unpacked[0], ma_last_time_unpacked[1]) + + assert mint_amount >= _min_mint_amount, "Slippage screwed you" + + # Mint pool tokens + total_supply += mint_amount + user_lp_token_bal: uint256 = self.balanceOf[_receiver] + + # here we can increase balance using unsafe add because + # user balance will always be <= total_supply. so if total_supply + # safeadd works, this can be safely unsafe: + self.balanceOf[_receiver] = unsafe_add(user_lp_token_bal, mint_amount) + self.total_supply = total_supply + log Transfer(empty(address), _receiver, mint_amount) + + log AddLiquidity( + msg.sender, + [_amounts[0], _amounts[1]], + [fees[0], fees[1]], + D1, + total_supply + ) + + return mint_amount + + +@external +@nonreentrant('lock') +def remove_liquidity_one_coin( + _burn_amount: uint256, + i: int128, + _min_received: uint256, + _receiver: address = msg.sender, +) -> uint256: + """ + @notice Withdraw a single coin from the pool + @param _burn_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @param _min_received Minimum amount of coin to receive + @param _receiver Address that receives the withdrawn coins + @return Amount of coin received + """ + assert _burn_amount > 0 # dev: do not remove 0 LP tokens + + dy: uint256 = 0 + fee: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + amp: uint256 = empty(uint256) + D: uint256 = empty(uint256) + + dy, fee, xp, amp, D = self._calc_withdraw_one_coin(_burn_amount, i) + assert dy >= _min_received, "Not enough coins removed" + + # fee * admin_fee / FEE_DENOMINATOR + self.admin_balances[i] += unsafe_div(fee * admin_fee, FEE_DENOMINATOR) + + self._burnFrom(msg.sender, _burn_amount) + + self._transfer_out(i, dy, _receiver) + + log RemoveLiquidityOne(msg.sender, i, _burn_amount, dy, self.total_supply) + + self.upkeep_oracles(xp, amp, D) + + return dy + + +@external +@nonreentrant('lock') +def remove_liquidity_imbalance( + _amounts: uint256[N_COINS], + _max_burn_amount: uint256, + _receiver: address = msg.sender +) -> uint256: + """ + @notice Withdraw coins from the pool in an imbalanced amount + @param _amounts List of amounts of underlying coins to withdraw + @param _max_burn_amount Maximum amount of LP token to burn in the withdrawal + @param _receiver Address that receives the withdrawn coins + @return Actual amount of the LP token burned in the withdrawal + """ + + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + old_balances: uint256[N_COINS] = self._balances() + D0: uint256 = self.get_D_mem(rates, old_balances, amp) + new_balances: uint256[N_COINS] = old_balances + + for i in range(N_COINS_128): + + if _amounts[i] != 0: + new_balances[i] -= _amounts[i] + self._transfer_out(i, _amounts[i], _receiver) + + D1: uint256 = self.get_D_mem(rates, new_balances, amp) + # base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + base_fee: uint256 = unsafe_div(unsafe_mul(self.fee, N_COINS), 4) + # ys: uint256 = (D0 + D1) / N_COINS + ys: uint256 = unsafe_div(D0 + D1, N_COINS) + + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + dynamic_fee: uint256 = 0 + xs: uint256 = 0 + ideal_balance: uint256 = 0 + difference: uint256 = 0 + new_balance: uint256 = 0 + + for i in range(N_COINS_128): + + ideal_balance = D1 * old_balances[i] / D0 + new_balance = new_balances[i] + + if ideal_balance > new_balance: + difference = unsafe_sub(ideal_balance, new_balance) + else: + difference = unsafe_sub(new_balance, ideal_balance) + + # base_fee * difference / FEE_DENOMINATOR + xs = unsafe_div(rates[i] * (old_balances[i] + new_balance), PRECISION) + dynamic_fee = self._dynamic_fee(xs, ys, base_fee) + fees[i] = unsafe_div(dynamic_fee * difference, FEE_DENOMINATOR) + + # fees[i] * admin_fee / FEE_DENOMINATOR + self.admin_balances[i] += unsafe_div(fees[i] * admin_fee, FEE_DENOMINATOR) + + new_balances[i] -= fees[i] + + D1 = self.get_D_mem(rates, new_balances, amp) # dev: reuse D1 for new D. + self.upkeep_oracles(self._xp_mem(rates, new_balances), amp, D1) + + total_supply: uint256 = self.total_supply + # here we can do unsafe div by D0 because we did several safedivs: + # burn_amount: uint256 = ((D0 - D1) * total_supply / D0) + 1 + burn_amount: uint256 = unsafe_div((D0 - D1) * total_supply, D0) + 1 + assert burn_amount > 1 # dev: zero tokens burned + assert burn_amount <= _max_burn_amount, "Slippage screwed you" + + self._burnFrom(msg.sender, burn_amount) + + log RemoveLiquidityImbalance( + msg.sender, + [_amounts[0], _amounts[1]], + [fees[0], fees[1]], + D1, + total_supply - burn_amount + ) + + return burn_amount + + +@external +@nonreentrant('lock') +def remove_liquidity( + _burn_amount: uint256, + _min_amounts: uint256[N_COINS], + _receiver: address = msg.sender, + _claim_admin_fees: bool = True, +) -> uint256[N_COINS]: + """ + @notice Withdraw coins from the pool + @dev Withdrawal amounts are based on current deposit ratios + @param _burn_amount Quantity of LP tokens to burn in the withdrawal + @param _min_amounts Minimum amounts of underlying coins to receive + @param _receiver Address that receives the withdrawn coins + @return List of amounts of coins that were withdrawn + """ + total_supply: uint256 = self.total_supply + assert _burn_amount > 0 # dev: invalid _burn_amount + amounts: uint256[N_COINS] = empty(uint256[N_COINS]) + balances: uint256[N_COINS] = self._balances() + + value: uint256 = 0 + + for i in range(N_COINS_128): + + value = unsafe_div(balances[i] * _burn_amount, total_supply) + assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected" + amounts[i] = value + self._transfer_out(i, value, _receiver) + + self._burnFrom(msg.sender, _burn_amount) # dev: insufficient funds + + # --------------------------- Upkeep D_oracle ---------------------------- + + ma_last_time_unpacked: uint256[2] = self.unpack_2(self.ma_last_time) + last_D_packed_current: uint256 = self.last_D_packed + old_D: uint256 = last_D_packed_current & (2**128 - 1) + + self.last_D_packed = self.pack_2( + old_D - unsafe_div(old_D * _burn_amount, total_supply), # new_D = proportionally reduce D. + self._calc_moving_average( + last_D_packed_current, + self.D_ma_time, + ma_last_time_unpacked[1] + ) + ) + + if ma_last_time_unpacked[1] < block.timestamp: + ma_last_time_unpacked[1] = block.timestamp + self.ma_last_time = self.pack_2(ma_last_time_unpacked[0], ma_last_time_unpacked[1]) + + # ------------------------------- Log event ------------------------------ + + log RemoveLiquidity( + msg.sender, + [amounts[0], amounts[1]], + empty(DynArray[uint256, MAX_COINS]), + unsafe_sub(total_supply, _burn_amount) + ) + + # ------- Withdraw admin fees if _claim_admin_fees is set to True -------- + + if _claim_admin_fees: + self._withdraw_admin_fees() + + return [amounts[0], amounts[1]] + + +@external +def withdraw_admin_fees(): + """ + @notice Claim admin fees. Callable by anyone. + """ + self._withdraw_admin_fees() + + +# ------------------------ AMM Internal Functions ---------------------------- + + +@view +@internal +def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256) -> uint256: + + _offpeg_fee_multiplier: uint256 = self.offpeg_fee_multiplier + + # to remove dynamic fee: just set _offpeg_fee_multiplier less than FEE_DENOMINATOR + if _offpeg_fee_multiplier <= FEE_DENOMINATOR: + return _fee + + xps2: uint256 = (xpi + xpj) ** 2 + return unsafe_div( + unsafe_mul(_offpeg_fee_multiplier, _fee), + unsafe_add( + unsafe_sub(_offpeg_fee_multiplier, FEE_DENOMINATOR) * 4 * xpi * xpj / xps2, + FEE_DENOMINATOR + ) + ) + + +@internal +def __exchange( + x: uint256, + _xp: uint256[N_COINS], + rates: uint256[N_COINS], + i: int128, + j: int128, +) -> uint256: + + amp: uint256 = self._A() + D: uint256 = math.get_D([_xp[0], _xp[1]], amp, N_COINS) + y: uint256 = math.get_y(i, j, x, [_xp[0], _xp[1]], amp, D, N_COINS) + + dy: uint256 = _xp[j] - y - 1 # -1 just in case there were some rounding errors + dy_fee: uint256 = unsafe_div( + dy * self._dynamic_fee( + unsafe_div(_xp[i] + x, 2), unsafe_div(_xp[j] + y, 2), self.fee + ), + FEE_DENOMINATOR + ) + + # Convert all to real units + dy = (dy - dy_fee) * PRECISION / rates[j] + + # admin_fee = dy_fee * admin_fee / FEE_DENOMINATOR + self.admin_balances[j] += unsafe_div( + unsafe_div(dy_fee * admin_fee, FEE_DENOMINATOR) * PRECISION, + rates[j] # we can do unsafediv here because we did safediv before + ) + + # Calculate and store state prices: + xp: uint256[N_COINS] = _xp + xp[i] = x + xp[j] = y + # D is not changed because we did not apply a fee + self.upkeep_oracles(xp, amp, D) + + return dy + + +@internal +def _exchange( + sender: address, + i: int128, + j: int128, + _dx: uint256, + _min_dy: uint256, + receiver: address, + expect_optimistic_transfer: bool +) -> uint256: + + assert i != j # dev: coin index out of range + assert _dx > 0 # dev: do not exchange 0 coins + + rates: uint256[N_COINS] = self._stored_rates() + old_balances: uint256[N_COINS] = self._balances() + xp: uint256[N_COINS] = self._xp_mem(rates, old_balances) + + # --------------------------- Do Transfer in ----------------------------- + + # `dx` is whatever the pool received after ERC20 transfer: + dx: uint256 = self._transfer_in( + i, + -1, + _dx, + sender, + expect_optimistic_transfer + ) + + # ------------------------------- Exchange ------------------------------- + + # xp[i] + dx * rates[i] / PRECISION + x: uint256 = xp[i] + unsafe_div(dx * rates[i], PRECISION) + dy: uint256 = self.__exchange(x, xp, rates, i, j) + assert dy >= _min_dy, "Exchange resulted in fewer coins than expected" + + # --------------------------- Do Transfer out ---------------------------- + + self._transfer_out(j, dy, receiver) + + # ------------------------------------------------------------------------ + + log TokenExchange(msg.sender, i, dx, j, dy) + + return dy + + +@internal +def _meta_add_liquidity(dx: uint256, base_i: int128) -> uint256: + + if BASE_POOL_IS_NG: + + base_inputs: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + for i in range(BASE_N_COINS, bound=MAX_COINS): + if i == convert(base_i, uint256): + base_inputs.append(dx) + else: + base_inputs.append(0) + return StableSwapNG(BASE_POOL).add_liquidity(base_inputs, 0) + + coin_i: address = coins[MAX_METAPOOL_COIN_INDEX] + x: uint256 = ERC20(coin_i).balanceOf(self) + + if BASE_N_COINS == 2: + + base_inputs: uint256[2] = empty(uint256[2]) + base_inputs[base_i] = dx + StableSwap2(BASE_POOL).add_liquidity(base_inputs, 0) + + if BASE_N_COINS == 3: + + base_inputs: uint256[3] = empty(uint256[3]) + base_inputs[base_i] = dx + StableSwap3(BASE_POOL).add_liquidity(base_inputs, 0) + + return ERC20(coin_i).balanceOf(self) - x + + +@internal +def _withdraw_admin_fees(): + + fee_receiver: address = factory.fee_receiver() + if fee_receiver == empty(address): + return # Do nothing. + + admin_balances: DynArray[uint256, MAX_COINS] = self.admin_balances + for i in range(N_COINS_128): + + if admin_balances[i] > 0: + self._transfer_out(i, admin_balances[i], fee_receiver) + + self.admin_balances = [0, 0] + + +# --------------------------- AMM Math Functions ----------------------------- + + +@view +@internal +def _A() -> uint256: + """ + Handle ramping A up or down + """ + t1: uint256 = self.future_A_time + A1: uint256 = self.future_A + + if block.timestamp < t1: + A0: uint256 = self.initial_A + t0: uint256 = self.initial_A_time + # Expressions in uint256 cannot have negative numbers, thus "if" + if A1 > A0: + return A0 + unsafe_sub(A1, A0) * (block.timestamp - t0) / (t1 - t0) + else: + return A0 - unsafe_sub(A0, A1) * (block.timestamp - t0) / (t1 - t0) + + else: # when t1 == 0 or block.timestamp >= t1 + return A1 + + +@pure +@internal +def _xp_mem(_rates: uint256[N_COINS], _balances: uint256[N_COINS]) -> uint256[N_COINS]: + + result: uint256[N_COINS] = empty(uint256[N_COINS]) + for i in range(N_COINS_128): + # _rates[i] * _balances[i] / PRECISION + result[i] = unsafe_div(_rates[i] * _balances[i], PRECISION) + + return result + + +@view +@internal +def get_D_mem( + _rates: uint256[N_COINS], + _balances: uint256[N_COINS], + _amp: uint256 +) -> uint256: + xp: uint256[N_COINS] = self._xp_mem(_rates, _balances) + return math.get_D([xp[0], xp[1]], _amp, N_COINS) + + +@view +@internal +def _calc_withdraw_one_coin( + _burn_amount: uint256, + i: int128 +) -> ( + uint256, + uint256, + uint256[N_COINS], + uint256, + uint256 +): + + # First, need to: + # * Get current D + # * Solve Eqn against y_i for D - _token_amount + + # get pool state + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + xp: uint256[N_COINS] = self._xp_mem(rates, self._balances()) + D0: uint256 = math.get_D([xp[0], xp[1]], amp, N_COINS) + + total_supply: uint256 = self.total_supply + D1: uint256 = D0 - _burn_amount * D0 / total_supply + new_y: uint256 = math.get_y_D(amp, i, [xp[0], xp[1]], D1, N_COINS) + + base_fee: uint256 = unsafe_div(unsafe_mul(self.fee, N_COINS), 4) + xp_reduced: uint256[N_COINS] = xp + # ys: uint256 = (D0 + D1) / (2 * N_COINS) + ys: uint256 = unsafe_div((D0 + D1), 4) + # base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + + dx_expected: uint256 = 0 + xp_j: uint256 = 0 + xavg: uint256 = 0 + dynamic_fee: uint256 = 0 + + for j in range(N_COINS_128): + + dx_expected = 0 + xp_j = xp[j] + if j == i: + dx_expected = xp_j * D1 / D0 - new_y + xavg = unsafe_div(xp_j + new_y, 2) + else: + dx_expected = xp_j - xp_j * D1 / D0 + xavg = xp_j + + # xp_j - dynamic_fee * dx_expected / FEE_DENOMINATOR + dynamic_fee = self._dynamic_fee(xavg, ys, base_fee) + xp_reduced[j] = xp_j - unsafe_div(dynamic_fee * dx_expected, FEE_DENOMINATOR) + + dy: uint256 = xp_reduced[i] - math.get_y_D(amp, i, [xp_reduced[0], xp_reduced[1]], D1, N_COINS) + dy_0: uint256 = (xp[i] - new_y) * PRECISION / rates[i] # w/o fees + dy = unsafe_div((dy - 1) * PRECISION, rates[i]) # Withdraw less to account for rounding errors + + # calculate state price + xp[i] = new_y + + return dy, dy_0 - dy, xp, amp, D1 + + +# -------------------------- AMM Price Methods ------------------------------- + +@pure +@internal +def pack_2(p1: uint256, p2: uint256) -> uint256: + assert p1 < 2**128 + assert p2 < 2**128 + return p1 | (p2 << 128) + + +@pure +@internal +def unpack_2(packed: uint256) -> uint256[2]: + return [packed & (2**128 - 1), packed >> 128] + + +@internal +@pure +def _get_p( + xp: uint256[N_COINS], + amp: uint256, + D: uint256, +) -> uint256: + + # dx_0 / dx_1 only, however can have any number of coins in pool + ANN: uint256 = unsafe_mul(amp, N_COINS) + Dr: uint256 = unsafe_div(D, pow_mod256(N_COINS, N_COINS)) + + for i in range(N_COINS_128): + Dr = Dr * D / xp[i] + + # ANN * xp[0] / A_PRECISION + xp0_A: uint256 = unsafe_div(ANN * xp[0], A_PRECISION) + return 10**18 * (xp0_A + unsafe_div(Dr * xp[0], xp[1])) / (xp0_A + Dr) + + +@internal +def upkeep_oracles(xp: uint256[N_COINS], amp: uint256, D: uint256): + """ + @notice Upkeeps price and D oracles. + """ + ma_last_time_unpacked: uint256[2] = self.unpack_2(self.ma_last_time) + last_prices_packed_current: uint256 = self.last_prices_packed + last_prices_packed_new: uint256 = last_prices_packed_current + + spot_price: uint256 = self._get_p(xp, amp, D) + + # -------------------------- Upkeep price oracle ------------------------- + + # Metapools are always 2-coin pools, so we care about idx=0 only: + if spot_price != 0: + + # Update packed prices ----------------- + last_prices_packed_new = self.pack_2( + min(spot_price, 2 * 10**18), # <----- Cap spot value by 2. + self._calc_moving_average( + last_prices_packed_current, + self.ma_exp_time, + ma_last_time_unpacked[0], # index 0 is ma_exp_time for prices + ) + ) + + self.last_prices_packed = last_prices_packed_new + + # ---------------------------- Upkeep D oracle --------------------------- + + self.last_D_packed = self.pack_2( + D, + self._calc_moving_average( + self.last_D_packed, + self.D_ma_time, + ma_last_time_unpacked[1], # index 1 is ma_exp_time for D + ) + ) + + # Housekeeping: Update ma_last_time for p and D oracles ------------------ + for i in range(2): + if ma_last_time_unpacked[i] < block.timestamp: + ma_last_time_unpacked[i] = block.timestamp + + self.ma_last_time = self.pack_2(ma_last_time_unpacked[0], ma_last_time_unpacked[1]) + + +@internal +@view +def _calc_moving_average( + packed_value: uint256, + averaging_window: uint256, + ma_last_time: uint256 +) -> uint256: + + last_spot_value: uint256 = packed_value & (2**128 - 1) + last_ema_value: uint256 = (packed_value >> 128) + + if ma_last_time < block.timestamp: # calculate new_ema_value and return that. + alpha: uint256 = math.exp( + -convert( + unsafe_div(unsafe_mul(unsafe_sub(block.timestamp, ma_last_time), 10**18), averaging_window), int256 + ) + ) + return unsafe_div(last_spot_value * (10**18 - alpha) + last_ema_value * alpha, 10**18) + + return last_ema_value + + +@view +@external +def last_price(i: uint256) -> uint256: + assert i == 0 # dev: metapools do not have last_price indices greater than 0. + return self.last_prices_packed & (2**128 - 1) + + +@view +@external +def ema_price(i: uint256) -> uint256: + assert i == 0 # dev: metapools do not have ema_price indices greater than 0. + return (self.last_prices_packed >> 128) + + +@external +@view +def get_p(i: uint256) -> uint256: + """ + @notice Returns the AMM State price of token + @dev if i = 0, it will return the state price of coin[1]. + @param i index of state price (0 for coin[1], 1 for coin[2], ...) + @return uint256 The state price quoted by the AMM for coin[i+1] + """ + assert i == 0 # dev: metapools do not have get_p indices greater than 0. + + amp: uint256 = self._A() + xp: uint256[N_COINS] = self._xp_mem( + self._stored_rates(), self._balances() + ) + D: uint256 = math.get_D([xp[0], xp[1]], amp, N_COINS) + return self._get_p(xp, amp, D) + + +@external +@view +@nonreentrant('lock') +def price_oracle(i: uint256) -> uint256: + assert i == 0 # dev: metapools do not have price_oracle indices greater than 0. + return self._calc_moving_average( + self.last_prices_packed, + self.ma_exp_time, + self.ma_last_time & (2**128 - 1), + ) + + +@external +@view +@nonreentrant('lock') +def D_oracle() -> uint256: + return self._calc_moving_average( + self.last_D_packed, + self.D_ma_time, + self.ma_last_time >> 128 + ) + + +# ---------------------------- ERC20 Utils ----------------------------------- + + +@view +@internal +def _domain_separator() -> bytes32: + if chain.id != CACHED_CHAIN_ID: + return keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + return CACHED_DOMAIN_SEPARATOR + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + # # NOTE: vyper does not allow underflows + # # so the following subtraction would revert on insufficient balance + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + + log Transfer(_from, _to, _value) + + +@internal +def _burnFrom(_from: address, _burn_amount: uint256): + + self.total_supply -= _burn_amount + self.balanceOf[_from] -= _burn_amount + log Transfer(_from, empty(address), _burn_amount) + + +@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. + """ + self._transfer(msg.sender, _to, _value) + return True + + +@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 + """ + self._transfer(_from, _to, _value) + + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != max_value(uint256): + _new_allowance: uint256 = _allowance - _value + self.allowance[_from][msg.sender] = _new_allowance + log Approval(_from, msg.sender, _new_allowance) + + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @notice Approve the passed address to transfer the specified amount of + tokens on behalf of msg.sender + @dev Beware that changing an allowance via this method brings the risk that + someone may use both the old and new allowance by unfortunate transaction + ordering: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will transfer the funds + @param _value The amount of tokens that may be transferred + @return bool success + """ + self.allowance[msg.sender][_spender] = _value + + log Approval(msg.sender, _spender, _value) + return True + + +@external +def permit( + _owner: address, + _spender: address, + _value: uint256, + _deadline: uint256, + _v: uint8, + _r: bytes32, + _s: bytes32 +) -> bool: + """ + @notice Approves spender by owner's signature to expend owner's tokens. + See https://eips.ethereum.org/EIPS/eip-2612. + @dev Inspired by https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L753-L793 + @dev Supports smart contract wallets which implement ERC1271 + https://eips.ethereum.org/EIPS/eip-1271 + @param _owner The address which is a source of funds and has signed the Permit. + @param _spender The address which is allowed to spend the funds. + @param _value The amount of tokens to be spent. + @param _deadline The timestamp after which the Permit is no longer valid. + @param _v The bytes[64] of the valid secp256k1 signature of permit by owner + @param _r The bytes[0:32] of the valid secp256k1 signature of permit by owner + @param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner + @return True, if transaction completes successfully + """ + assert _owner != empty(address) + assert block.timestamp <= _deadline + + 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)) + ) + ) + + if _owner.is_contract: + sig: Bytes[65] = concat(_abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1)) + # reentrancy not a concern since this is a staticcall + assert ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL + else: + assert ecrecover(digest, convert(_v, uint256), convert(_r, uint256), convert(_s, uint256)) == _owner + + self.allowance[_owner][_spender] = _value + self.nonces[_owner] = unsafe_add(nonce, 1) + + log Approval(_owner, _spender, _value) + return True + + +@view +@external +def DOMAIN_SEPARATOR() -> bytes32: + """ + @notice EIP712 domain separator. + @return bytes32 Domain Separator set for the current chain. + """ + return self._domain_separator() + + +# ------------------------- AMM View Functions ------------------------------- + + +@view +@external +def get_dx(i: int128, j: int128, dy: uint256) -> uint256: + """ + @notice Calculate the current input dx given output dy + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param dy Amount of `j` being received after exchange + @return Amount of `i` predicted + """ + return StableSwapViews(factory.views_implementation()).get_dx(i, j, dy, self) + + +@view +@external +def get_dx_underlying(i: int128, j: int128, dy: uint256) -> uint256: + """ + @notice Calculate the current input dx given output dy + @dev Swap involves base pool tokens (either i or j should be 0); + If not, this method reverts. + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param dy Amount of `j` being received after exchange + @return Amount of `i` predicted + """ + return StableSwapViews(factory.views_implementation()).get_dx_underlying(i, j, dy, self) + + +@view +@external +def get_dy(i: int128, j: int128, dx: uint256) -> uint256: + """ + @notice Calculate the current output dy given input dx + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param dx Amount of `i` being exchanged + @return Amount of `j` predicted + """ + return StableSwapViews(factory.views_implementation()).get_dy(i, j, dx, self) + + +@view +@external +def get_dy_underlying(i: int128, j: int128, dx: uint256) -> uint256: + """ + @notice Calculate the current output dy given input dx + @dev Swap involves base pool tokens (either i or j should be 0); + If not, this method reverts. + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param dx Amount of `i` being exchanged + @return Amount of `j` predicted + """ + return StableSwapViews(factory.views_implementation()).get_dy_underlying(i, j, dx, self) + + +@view +@external +def calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256: + """ + @notice Calculate the amount received when withdrawing a single coin + @param _burn_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @return Amount of coin received + """ + return self._calc_withdraw_one_coin(_burn_amount, i)[0] + + +@view +@external +@nonreentrant('lock') +def totalSupply() -> uint256: + """ + @notice The total supply of pool LP tokens + @return self.total_supply, 18 decimals. + """ + return self.total_supply + + +@view +@external +@nonreentrant('lock') +def get_virtual_price() -> uint256: + """ + @notice The current virtual price of the pool LP token + @dev Useful for calculating profits. + The method may be vulnerable to donation-style attacks if implementation + contains rebasing tokens. For integrators, caution is advised. + @return LP token virtual price normalized to 1e18 + """ + xp: uint256[N_COINS] = self._xp_mem(self._stored_rates(), self._balances()) + D: uint256 = math.get_D([xp[0], xp[1]], self._A(), N_COINS) + # D is in the units similar to DAI (e.g. converted to precision 1e18) + # When balanced, D = n * x_u - total virtual value of the portfolio + return D * PRECISION / self.total_supply + + +@view +@external +def calc_token_amount( + _amounts: uint256[N_COINS], + _is_deposit: bool +) -> uint256: + """ + @notice Calculate addition or reduction in token supply from a deposit or withdrawal + @param _amounts Amount of each coin being deposited + @param _is_deposit set True for deposits, False for withdrawals + @return Expected amount of LP tokens received + """ + return StableSwapViews(factory.views_implementation()).calc_token_amount( + [_amounts[0], _amounts[1]], + _is_deposit, + self + ) + + +@view +@external +def A() -> uint256: + return unsafe_div(self._A(), A_PRECISION) + + +@view +@external +def A_precise() -> uint256: + return self._A() + + +@view +@external +def balances(i: uint256) -> uint256: + """ + @notice Get the current balance of a coin within the + pool, less the accrued admin fees + @param i Index value for the coin to query balance of + @return Token balance + """ + return self._balances()[i] + + +@view +@external +def get_balances() -> DynArray[uint256, MAX_COINS]: + balances: uint256[N_COINS] = self._balances() + return [balances[0], balances[1]] + + +@view +@external +def stored_rates() -> DynArray[uint256, MAX_COINS]: + rates: uint256[N_COINS] = self._stored_rates() + return [rates[0], rates[1]] + + +@view +@external +def dynamic_fee(i: int128, j: int128) -> uint256: + """ + @notice Return the fee for swapping between `i` and `j` + @param i Index value for the coin to send + @param j Index value of the coin to receive + @return Swap fee expressed as an integer with 1e10 precision + """ + return StableSwapViews(factory.views_implementation()).dynamic_fee(i, j, self) + + +# --------------------------- AMM Admin Functions ---------------------------- + + +@external +def ramp_A(_future_A: uint256, _future_time: uint256): + assert msg.sender == factory.admin() # dev: only owner + assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME + assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time + + _initial_A: uint256 = self._A() + _future_A_p: uint256 = _future_A * A_PRECISION + + assert _future_A > 0 and _future_A < MAX_A + if _future_A_p < _initial_A: + assert _future_A_p * MAX_A_CHANGE >= _initial_A + else: + assert _future_A_p <= _initial_A * MAX_A_CHANGE + + self.initial_A = _initial_A + self.future_A = _future_A_p + self.initial_A_time = block.timestamp + self.future_A_time = _future_time + + log RampA(_initial_A, _future_A_p, block.timestamp, _future_time) + + +@external +def stop_ramp_A(): + assert msg.sender == factory.admin() # dev: only owner + + current_A: uint256 = self._A() + self.initial_A = current_A + self.future_A = current_A + self.initial_A_time = block.timestamp + self.future_A_time = block.timestamp + # now (block.timestamp < t1) is always False, so we return saved A + + log StopRampA(current_A, block.timestamp) + + +@external +def set_new_fee(_new_fee: uint256, _new_offpeg_fee_multiplier: uint256): + + assert msg.sender == factory.admin() + + # set new fee: + assert _new_fee <= MAX_FEE + self.fee = _new_fee + + # set new offpeg_fee_multiplier: + assert _new_offpeg_fee_multiplier * _new_fee <= MAX_FEE * FEE_DENOMINATOR # dev: offpeg multiplier exceeds maximum + self.offpeg_fee_multiplier = _new_offpeg_fee_multiplier + + log ApplyNewFee(_new_fee, _new_offpeg_fee_multiplier) + + +@external +def set_ma_exp_time(_ma_exp_time: uint256, _D_ma_time: uint256): + """ + @notice Set the moving average window of the price oracles. + @param _ma_exp_time Moving average window for the price oracle. It is time_in_seconds / ln(2). + @param _D_ma_time Moving average window for the D oracle. It is time_in_seconds / ln(2). + """ + assert msg.sender == factory.admin() # dev: only owner + assert unsafe_mul(_ma_exp_time, _D_ma_time) > 0 # dev: 0 in input values + + self.ma_exp_time = _ma_exp_time + self.D_ma_time = _D_ma_time + + log SetNewMATime(_ma_exp_time, _D_ma_time) diff --git a/contracts/amms/stableswapng/CurveStableSwapNG.vy b/contracts/amms/stableswapng/CurveStableSwapNG.vy new file mode 100644 index 0000000..f192932 --- /dev/null +++ b/contracts/amms/stableswapng/CurveStableSwapNG.vy @@ -0,0 +1,1890 @@ +# pragma version 0.3.10 +# pragma optimize codesize +# pragma evm-version shanghai +""" +@title CurveStableSwapNG +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Stableswap implementation for up to 8 coins with no rehypothecation, + i.e. the AMM does not deposit tokens into other contracts. The Pool contract also + records exponential moving averages for coins relative to coin 0. +@dev Asset Types: + 0. Standard ERC20 token with no additional features. + Note: Users are advised to do careful due-diligence on + ERC20 tokens that they interact with, as this + contract cannot differentiate between harmless and + malicious ERC20 tokens. + 1. Oracle - token with rate oracle (e.g. wstETH) + Note: Oracles may be controlled externally by an EOA. Users + are advised to proceed with caution. + 2. Rebasing - token with rebase (e.g. stETH). + Note: Users and Integrators are advised to understand how + the AMM contract works with rebasing balances. + 3. ERC4626 - token with convertToAssets method (e.g. sDAI). + Note: Some ERC4626 implementations may be susceptible to + Donation/Inflation attacks. Users are advised to + proceed with caution. + NOTE: Pool Cannot support tokens with multiple asset types: e.g. ERC4626 + with fees are not supported. + Supports: + 1. ERC20 support for return True/revert, return True/False, return None + 2. ERC20 tokens can have arbitrary decimals (<=18). + 3. ERC20 tokens that rebase (either positive or fee on transfer) + 4. ERC20 tokens that have a rate oracle (e.g. wstETH, cbETH, sDAI, etc.) + Note: Oracle precision _must_ be 10**18. + 5. ERC4626 tokens with arbitrary precision (<=18) of Vault token and underlying + asset. + Additional features include: + 1. Adds price oracles based on AMM State Price (and _not_ last traded price). + 2. Adds TVL oracle based on D. + 3. `exchange_received`: swaps that expect an ERC20 transfer to have occurred + prior to executing the swap. + Note: a. If pool contains rebasing tokens and one of the `asset_types` is 2 (Rebasing) + then calling `exchange_received` will REVERT. + b. If pool contains rebasing token and `asset_types` does not contain 2 (Rebasing) + then this is an incorrect implementation and rebases can be + stolen. + 4. Adds `get_dx`: Similar to `get_dy` which returns an expected output + of coin[j] for given `dx` amount of coin[i], `get_dx` returns expected + input of coin[i] for an output amount of coin[j]. + 5. Fees are dynamic: AMM will charge a higher fee if pool depegs. This can cause very + slight discrepancies between calculated fees and realised fees. +""" + +from vyper.interfaces import ERC20 +from vyper.interfaces import ERC20Detailed +from vyper.interfaces import ERC4626 + +implements: ERC20 + +# ------------------------------- Interfaces --------------------------------- + +interface Factory: + def fee_receiver() -> address: view + def admin() -> address: view + def views_implementation() -> address: view + +interface ERC1271: + def isValidSignature(_hash: bytes32, _signature: Bytes[65]) -> bytes32: view + +interface StableSwapViews: + def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view + def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view + def dynamic_fee(i: int128, j: int128, pool: address) -> uint256: view + def calc_token_amount( + _amounts: DynArray[uint256, MAX_COINS], + _is_deposit: bool, + _pool: 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: int128 + tokens_sold: uint256 + bought_id: int128 + tokens_bought: uint256 + +event TokenExchangeUnderlying: + buyer: indexed(address) + sold_id: int128 + tokens_sold: uint256 + bought_id: int128 + tokens_bought: uint256 + +event AddLiquidity: + provider: indexed(address) + token_amounts: DynArray[uint256, MAX_COINS] + fees: DynArray[uint256, MAX_COINS] + invariant: uint256 + token_supply: uint256 + +event RemoveLiquidity: + provider: indexed(address) + token_amounts: DynArray[uint256, MAX_COINS] + fees: DynArray[uint256, MAX_COINS] + token_supply: uint256 + +event RemoveLiquidityOne: + provider: indexed(address) + token_id: int128 + token_amount: uint256 + coin_amount: uint256 + token_supply: uint256 + +event RemoveLiquidityImbalance: + provider: indexed(address) + token_amounts: DynArray[uint256, MAX_COINS] + fees: DynArray[uint256, MAX_COINS] + invariant: uint256 + token_supply: uint256 + +event RampA: + old_A: uint256 + new_A: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + A: uint256 + t: uint256 + +event ApplyNewFee: + fee: uint256 + offpeg_fee_multiplier: uint256 + +event SetNewMATime: + ma_exp_time: uint256 + D_ma_time: uint256 + + +MAX_COINS: constant(uint256) = 8 # max coins is 8 in the factory +MAX_COINS_128: constant(int128) = 8 + +# ---------------------------- Pool Variables -------------------------------- + +N_COINS: public(immutable(uint256)) +N_COINS_128: immutable(int128) +PRECISION: constant(uint256) = 10 ** 18 + +factory: immutable(Factory) +coins: public(immutable(DynArray[address, MAX_COINS])) +asset_types: immutable(DynArray[uint8, MAX_COINS]) +pool_contains_rebasing_tokens: immutable(bool) +stored_balances: DynArray[uint256, MAX_COINS] + +# Fee specific vars +FEE_DENOMINATOR: constant(uint256) = 10 ** 10 +fee: public(uint256) # fee * 1e10 +offpeg_fee_multiplier: public(uint256) # * 1e10 +admin_fee: public(constant(uint256)) = 5000000000 +MAX_FEE: constant(uint256) = 5 * 10 ** 9 + +# ---------------------- Pool Amplification Parameters ----------------------- + +A_PRECISION: constant(uint256) = 100 +MAX_A: constant(uint256) = 10 ** 6 +MAX_A_CHANGE: constant(uint256) = 10 + +initial_A: public(uint256) +future_A: public(uint256) +initial_A_time: public(uint256) +future_A_time: public(uint256) + +# ---------------------------- Admin Variables ------------------------------- + +MIN_RAMP_TIME: constant(uint256) = 86400 +admin_balances: public(DynArray[uint256, MAX_COINS]) + +# ----------------------- Oracle Specific vars ------------------------------- + +rate_multipliers: immutable(DynArray[uint256, MAX_COINS]) +# [bytes4 method_id][bytes8 ][bytes20 oracle] +rate_oracles: immutable(DynArray[uint256, MAX_COINS]) + +# For ERC4626 tokens, we need: +call_amount: immutable(DynArray[uint256, MAX_COINS]) +scale_factor: immutable(DynArray[uint256, MAX_COINS]) + +last_prices_packed: DynArray[uint256, MAX_COINS] # packing: last_price, ma_price +last_D_packed: uint256 # packing: last_D, ma_D +ma_exp_time: public(uint256) +D_ma_time: public(uint256) +ma_last_time: public(uint256) # packing: ma_last_time_p, ma_last_time_D +# ma_last_time has a distinction for p and D because p is _not_ updated if +# users remove_liquidity, but D is. + +# shift(2**32 - 1, 224) +ORACLE_BIT_MASK: constant(uint256) = (2**32 - 1) * 256**28 + +# --------------------------- ERC20 Specific Vars ---------------------------- + +name: public(immutable(String[64])) +symbol: public(immutable(String[32])) +decimals: public(constant(uint8)) = 18 +version: public(constant(String[8])) = "v7.0.0" + +balanceOf: public(HashMap[address, uint256]) +allowance: public(HashMap[address, HashMap[address, uint256]]) +total_supply: uint256 +nonces: public(HashMap[address, uint256]) + +# keccak256("isValidSignature(bytes32,bytes)")[:4] << 224 +ERC1271_MAGIC_VAL: constant(bytes32) = 0x1626ba7e00000000000000000000000000000000000000000000000000000000 +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) + + +# ------------------------------ AMM Setup ----------------------------------- + + +@external +def __init__( + _name: String[32], + _symbol: String[10], + _A: uint256, + _fee: uint256, + _offpeg_fee_multiplier: uint256, + _ma_exp_time: uint256, + _coins: DynArray[address, MAX_COINS], + _rate_multipliers: DynArray[uint256, MAX_COINS], + _asset_types: DynArray[uint8, MAX_COINS], + _method_ids: DynArray[bytes4, MAX_COINS], + _oracles: DynArray[address, MAX_COINS], +): + """ + @notice Initialize the pool contract + @param _name Name of the new plain pool. + @param _symbol Symbol for the new plain pool. + @param _A Amplification co-efficient - a lower value here means + less tolerance for imbalance within the pool's assets. + Suggested values include: + * Uncollateralized algorithmic stablecoins: 5-10 + * Non-redeemable, collateralized assets: 100 + * Redeemable assets: 200-400 + @param _fee Trade fee, given as an integer with 1e10 precision. The + the maximum is 1% (100000000). + 50% of the fee is distributed to veCRV holders. + @param _offpeg_fee_multiplier A multiplier that determines how much to increase + Fees by when assets in the AMM depeg. Example value: 20000000000 + @param _ma_exp_time Averaging window of oracle. Set as time_in_seconds / ln(2) + Example: for 10 minute EMA, _ma_exp_time is 600 / ln(2) ~= 866 + @param _coins List of addresses of the coins being used in the pool. + @param _rate_multipliers An array of: [10 ** (36 - _coins[n].decimals()), ... for n in range(N_COINS)] + @param _asset_types Array of uint8 representing tokens in pool + @param _method_ids Array of first four bytes of the Keccak-256 hash of the function signatures + of the oracle addresses that gives rate oracles. + Calculated as: keccak(text=event_signature.replace(" ", ""))[:4] + @param _oracles Array of rate oracle addresses. + """ + + coins = _coins + asset_types = _asset_types + pool_contains_rebasing_tokens = 2 in asset_types + __n_coins: uint256 = len(_coins) + N_COINS = __n_coins + N_COINS_128 = convert(__n_coins, int128) + + rate_multipliers = _rate_multipliers + + factory = Factory(msg.sender) + + A: uint256 = unsafe_mul(_A, A_PRECISION) + self.initial_A = A + self.future_A = A + self.fee = _fee + self.offpeg_fee_multiplier = _offpeg_fee_multiplier + + assert _ma_exp_time != 0 + self.ma_exp_time = _ma_exp_time + self.D_ma_time = 62324 # <--------- 12 hours default on contract start. + self.ma_last_time = self.pack_2(block.timestamp, block.timestamp) + + # ------------------- initialize storage for DynArrays ------------------ + + _call_amount: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + _scale_factor: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + _rate_oracles: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + for i in range(N_COINS_128, bound=MAX_COINS_128): + + if i < N_COINS_128 - 1: + self.last_prices_packed.append(self.pack_2(10**18, 10**18)) + + _rate_oracles.append(convert(_method_ids[i], uint256) * 2**224 | convert(_oracles[i], uint256)) + self.stored_balances.append(0) + self.admin_balances.append(0) + + if _asset_types[i] == 3: + + _call_amount.append(10**convert(ERC20Detailed(_coins[i]).decimals(), uint256)) + _underlying_asset: address = ERC4626(_coins[i]).asset() + _scale_factor.append(10**(18 - convert(ERC20Detailed(_underlying_asset).decimals(), uint256))) + + else: + + _call_amount.append(0) + _scale_factor.append(0) + + call_amount = _call_amount + scale_factor = _scale_factor + rate_oracles = _rate_oracles + + # ----------------------------- ERC20 stuff ------------------------------ + + name = _name + symbol = _symbol + + # EIP712 related params ----------------- + NAME_HASH = keccak256(name) + salt = block.prevhash + CACHED_CHAIN_ID = chain.id + CACHED_DOMAIN_SEPARATOR = keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + + # ------------------------ Fire a transfer event ------------------------- + + log Transfer(empty(address), msg.sender, 0) + + +# ------------------ Token transfers in and out of the AMM ------------------- + + +@internal +def _transfer_in( + coin_idx: int128, + dx: uint256, + sender: address, + expect_optimistic_transfer: bool, +) -> uint256: + """ + @notice Contains all logic to handle ERC20 token transfers. + @param coin_idx Index of the coin to transfer in. + @param dx amount of `_coin` to transfer into the pool. + @param sender address to transfer `_coin` from. + @param receiver address to transfer `_coin` to. + @param expect_optimistic_transfer True if contract expects an optimistic coin transfer + """ + _dx: uint256 = ERC20(coins[coin_idx]).balanceOf(self) + + # ------------------------- Handle Transfers ----------------------------- + + if expect_optimistic_transfer: + + _dx = _dx - self.stored_balances[coin_idx] + assert _dx >= dx + + else: + + assert dx > 0 # dev : do not transferFrom 0 tokens into the pool + assert ERC20(coins[coin_idx]).transferFrom( + sender, self, dx, default_return_value=True + ) + + _dx = ERC20(coins[coin_idx]).balanceOf(self) - _dx + + # --------------------------- Store transferred in amount --------------------------- + + self.stored_balances[coin_idx] += _dx + + return _dx + + +@internal +def _transfer_out(_coin_idx: int128, _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_coin`, `_exchange`, `_withdraw_admin_fees` and + `remove_liquidity_imbalance` methods. + @param _coin_idx Index of the token to transfer out + @param _amount Amount of token to transfer out + @param receiver Address to send the tokens to + """ + assert receiver != empty(address) # dev: do not send tokens to zero_address + + if not pool_contains_rebasing_tokens: + + # we need not cache balanceOf pool before swap out + self.stored_balances[_coin_idx] -= _amount + assert ERC20(coins[_coin_idx]).transfer( + receiver, _amount, default_return_value=True + ) + + else: + + # cache balances pre and post to account for fee on transfers etc. + coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) + assert ERC20(coins[_coin_idx]).transfer( + receiver, _amount, default_return_value=True + ) + self.stored_balances[_coin_idx] = coin_balance - _amount + + +# -------------------------- AMM Special Methods ----------------------------- + + +@view +@internal +def _stored_rates() -> DynArray[uint256, MAX_COINS]: + """ + @notice Gets rate multipliers for each coin. + @dev If the coin has a rate oracle that has been properly initialised, + this method queries that rate by static-calling an external + contract. + """ + rates: DynArray[uint256, MAX_COINS] = rate_multipliers + + for i in range(N_COINS_128, bound=MAX_COINS_128): + + if asset_types[i] == 1 and not rate_oracles[i] == 0: + + # NOTE: fetched_rate is assumed to be 10**18 precision + oracle_response: Bytes[32] = raw_call( + convert(rate_oracles[i] % 2**160, address), + _abi_encode(rate_oracles[i] & ORACLE_BIT_MASK), + max_outsize=32, + is_static_call=True, + ) + assert len(oracle_response) == 32 + fetched_rate: uint256 = convert(oracle_response, uint256) + + rates[i] = unsafe_div(rates[i] * fetched_rate, PRECISION) + + elif asset_types[i] == 3: # ERC4626 + + # fetched_rate: uint256 = ERC4626(coins[i]).convertToAssets(call_amount[i]) * scale_factor[i] + # here: call_amount has ERC4626 precision, but the returned value is scaled up to 18 + # using scale_factor which is (18 - n) if underlying asset has n decimals. + rates[i] = unsafe_div( + rates[i] * ERC4626(coins[i]).convertToAssets(call_amount[i]) * scale_factor[i], + PRECISION + ) # 1e18 precision + + return rates + + +@view +@internal +def _balances() -> DynArray[uint256, MAX_COINS]: + """ + @notice Calculates the pool's balances _excluding_ the admin's balances. + @dev If the pool contains rebasing tokens, this method ensures LPs keep all + rebases and admin only claims swap fees. This also means that, since + admin's balances are stored in an array and not inferred from read balances, + the fees in the rebasing token that the admin collects is immune to + slashing events. + """ + result: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + balances_i: uint256 = 0 + + for i in range(N_COINS_128, bound=MAX_COINS_128): + + if pool_contains_rebasing_tokens: + # Read balances by gulping to account for rebases + balances_i = ERC20(coins[i]).balanceOf(self) - self.admin_balances[i] + else: + # Use cached balances + balances_i = self.stored_balances[i] - self.admin_balances[i] + + result.append(balances_i) + + return result + + +# -------------------------- AMM Main Functions ------------------------------ + + +@external +@nonreentrant('lock') +def exchange( + i: int128, + j: int128, + _dx: uint256, + _min_dy: uint256, + _receiver: address = msg.sender, +) -> uint256: + """ + @notice Perform an exchange between two coins + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param _dx Amount of `i` being exchanged + @param _min_dy Minimum amount of `j` to receive + @param _receiver Address that receives `j` + @return Actual amount of `j` received + """ + return self._exchange( + msg.sender, + i, + j, + _dx, + _min_dy, + _receiver, + False + ) + + +@external +@nonreentrant('lock') +def exchange_received( + i: int128, + j: int128, + _dx: uint256, + _min_dy: uint256, + _receiver: address = msg.sender, +) -> uint256: + """ + @notice Perform an exchange between two coins without transferring token in + @dev The contract swaps tokens based on a change in balance of coin[i]. The + dx = ERC20(coin[i]).balanceOf(self) - self.stored_balances[i]. Users of + this method are dex aggregators, arbitrageurs, or other users who do not + wish to grant approvals to the contract: they would instead send tokens + directly to the contract and call `exchange_received`. + Note: This is disabled if pool contains rebasing tokens. + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param _dx Amount of `i` being exchanged + @param _min_dy Minimum amount of `j` to receive + @param _receiver Address that receives `j` + @return Actual amount of `j` received + """ + assert not pool_contains_rebasing_tokens # dev: exchange_received not supported if pool contains rebasing tokens + return self._exchange( + msg.sender, + i, + j, + _dx, + _min_dy, + _receiver, + True, # <--------------------------------------- swap optimistically. + ) + + +@external +@nonreentrant('lock') +def add_liquidity( + _amounts: DynArray[uint256, MAX_COINS], + _min_mint_amount: uint256, + _receiver: address = msg.sender +) -> uint256: + """ + @notice Deposit coins into the pool + @param _amounts List of amounts of coins to deposit + @param _min_mint_amount Minimum amount of LP tokens to mint from the deposit + @param _receiver Address that owns the minted LP tokens + @return Amount of LP tokens received by depositing + """ + assert _receiver != empty(address) # dev: do not send LP tokens to zero_address + + amp: uint256 = self._A() + old_balances: DynArray[uint256, MAX_COINS] = self._balances() + rates: DynArray[uint256, MAX_COINS] = self._stored_rates() + + # Initial invariant + D0: uint256 = self.get_D_mem(rates, old_balances, amp) + + total_supply: uint256 = self.total_supply + new_balances: DynArray[uint256, MAX_COINS] = old_balances + + # -------------------------- Do Transfers In ----------------------------- + + for i in range(N_COINS_128, bound=MAX_COINS_128): + + if _amounts[i] > 0: + + new_balances[i] += self._transfer_in( + i, + _amounts[i], + msg.sender, + False, # expect_optimistic_transfer + ) + + else: + + assert total_supply != 0 # dev: initial deposit requires all coins + + # ------------------------------------------------------------------------ + + # Invariant after change + D1: uint256 = self.get_D_mem(rates, new_balances, amp) + assert D1 > D0 + + # We need to recalculate the invariant accounting for fees + # to calculate fair user's share + fees: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + mint_amount: uint256 = 0 + + if total_supply > 0: + + ideal_balance: uint256 = 0 + difference: uint256 = 0 + new_balance: uint256 = 0 + + ys: uint256 = unsafe_div(D0 + D1, N_COINS) + xs: uint256 = 0 + _dynamic_fee_i: uint256 = 0 + + # Only account for fees if we are not the first to deposit + base_fee: uint256 = unsafe_div( + unsafe_mul(self.fee, N_COINS), + unsafe_mul(4, unsafe_sub(N_COINS, 1)) + ) + + for i in range(N_COINS_128, bound=MAX_COINS_128): + + ideal_balance = D1 * old_balances[i] / D0 + difference = 0 + new_balance = new_balances[i] + + if ideal_balance > new_balance: + difference = unsafe_sub(ideal_balance, new_balance) + else: + difference = unsafe_sub(new_balance, ideal_balance) + + # fee[i] = _dynamic_fee(i, j) * difference / FEE_DENOMINATOR + xs = unsafe_div(rates[i] * (old_balances[i] + new_balance), PRECISION) + _dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee) + fees.append(unsafe_div(_dynamic_fee_i * difference, FEE_DENOMINATOR)) + self.admin_balances[i] += unsafe_div(fees[i] * admin_fee, FEE_DENOMINATOR) + new_balances[i] -= fees[i] + + xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, new_balances) + D1 = self.get_D(xp, amp) # <--------------- Reuse D1 for new D value. + mint_amount = unsafe_div(total_supply * (D1 - D0), D0) + self.upkeep_oracles(xp, amp, D1) + + else: + + mint_amount = D1 # Take the dust if there was any + + # (re)instantiate D oracle if totalSupply is zero. + self.last_D_packed = self.pack_2(D1, D1) + + # Update D ma time: + ma_last_time_unpacked: uint256[2] = self.unpack_2(self.ma_last_time) + if ma_last_time_unpacked[1] < block.timestamp: + ma_last_time_unpacked[1] = block.timestamp + self.ma_last_time = self.pack_2(ma_last_time_unpacked[0], ma_last_time_unpacked[1]) + + assert mint_amount >= _min_mint_amount, "Slippage screwed you" + + # Mint pool tokens + total_supply += mint_amount + self.balanceOf[_receiver] += mint_amount + self.total_supply = total_supply + log Transfer(empty(address), _receiver, mint_amount) + + log AddLiquidity(msg.sender, _amounts, fees, D1, total_supply) + + return mint_amount + + +@external +@nonreentrant('lock') +def remove_liquidity_one_coin( + _burn_amount: uint256, + i: int128, + _min_received: uint256, + _receiver: address = msg.sender, +) -> uint256: + """ + @notice Withdraw a single coin from the pool + @param _burn_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @param _min_received Minimum amount of coin to receive + @param _receiver Address that receives the withdrawn coins + @return Amount of coin received + """ + assert _burn_amount > 0 # dev: do not remove 0 LP tokens + dy: uint256 = 0 + fee: uint256 = 0 + xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + amp: uint256 = empty(uint256) + D: uint256 = empty(uint256) + + dy, fee, xp, amp, D = self._calc_withdraw_one_coin(_burn_amount, i) + assert dy >= _min_received, "Not enough coins removed" + + self.admin_balances[i] += unsafe_div(fee * admin_fee, FEE_DENOMINATOR) + + self._burnFrom(msg.sender, _burn_amount) + + self._transfer_out(i, dy, _receiver) + + log RemoveLiquidityOne(msg.sender, i, _burn_amount, dy, self.total_supply) + + self.upkeep_oracles(xp, amp, D) + + return dy + + +@external +@nonreentrant('lock') +def remove_liquidity_imbalance( + _amounts: DynArray[uint256, MAX_COINS], + _max_burn_amount: uint256, + _receiver: address = msg.sender +) -> uint256: + """ + @notice Withdraw coins from the pool in an imbalanced amount + @param _amounts List of amounts of underlying coins to withdraw + @param _max_burn_amount Maximum amount of LP token to burn in the withdrawal + @param _receiver Address that receives the withdrawn coins + @return Actual amount of the LP token burned in the withdrawal + """ + amp: uint256 = self._A() + rates: DynArray[uint256, MAX_COINS] = self._stored_rates() + old_balances: DynArray[uint256, MAX_COINS] = self._balances() + D0: uint256 = self.get_D_mem(rates, old_balances, amp) + new_balances: DynArray[uint256, MAX_COINS] = old_balances + + for i in range(N_COINS_128, bound=MAX_COINS_128): + + if _amounts[i] != 0: + new_balances[i] -= _amounts[i] + self._transfer_out(i, _amounts[i], _receiver) + + D1: uint256 = self.get_D_mem(rates, new_balances, amp) + base_fee: uint256 = unsafe_div( + unsafe_mul(self.fee, N_COINS), + unsafe_mul(4, unsafe_sub(N_COINS, 1)) + ) + ys: uint256 = unsafe_div((D0 + D1), N_COINS) + + fees: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + dynamic_fee: uint256 = 0 + xs: uint256 = 0 + ideal_balance: uint256 = 0 + difference: uint256 = 0 + new_balance: uint256 = 0 + + for i in range(N_COINS_128, bound=MAX_COINS_128): + + ideal_balance = D1 * old_balances[i] / D0 + difference = 0 + new_balance = new_balances[i] + + if ideal_balance > new_balance: + difference = unsafe_sub(ideal_balance, new_balance) + else: + difference = unsafe_sub(new_balance, ideal_balance) + + xs = unsafe_div(rates[i] * (old_balances[i] + new_balance), PRECISION) + dynamic_fee = self._dynamic_fee(xs, ys, base_fee) + fees.append(unsafe_div(dynamic_fee * difference, FEE_DENOMINATOR)) + + self.admin_balances[i] += unsafe_div(fees[i] * admin_fee, FEE_DENOMINATOR) + new_balances[i] -= fees[i] + + D1 = self.get_D_mem(rates, new_balances, amp) # dev: reuse D1 for new D. + self.upkeep_oracles(self._xp_mem(rates, new_balances), amp, D1) + + total_supply: uint256 = self.total_supply + burn_amount: uint256 = unsafe_div((D0 - D1) * total_supply, D0) + 1 + assert burn_amount > 1 # dev: zero tokens burned + assert burn_amount <= _max_burn_amount, "Slippage screwed you" + + self._burnFrom(msg.sender, burn_amount) + + log RemoveLiquidityImbalance( + msg.sender, + _amounts, + fees, + D1, + total_supply - burn_amount + ) + + return burn_amount + + +@external +@nonreentrant('lock') +def remove_liquidity( + _burn_amount: uint256, + _min_amounts: DynArray[uint256, MAX_COINS], + _receiver: address = msg.sender, + _claim_admin_fees: bool = True, +) -> DynArray[uint256, MAX_COINS]: + """ + @notice Withdraw coins from the pool + @dev Withdrawal amounts are based on current deposit ratios + @param _burn_amount Quantity of LP tokens to burn in the withdrawal + @param _min_amounts Minimum amounts of underlying coins to receive + @param _receiver Address that receives the withdrawn coins + @return List of amounts of coins that were withdrawn + """ + total_supply: uint256 = self.total_supply + assert _burn_amount > 0 # dev: invalid burn amount + assert len(_min_amounts) == N_COINS # dev: invalid array length for _min_amounts + + amounts: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + balances: DynArray[uint256, MAX_COINS] = self._balances() + + value: uint256 = 0 + for i in range(N_COINS_128, bound=MAX_COINS_128): + + value = unsafe_div(balances[i] * _burn_amount, total_supply) + assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected" + amounts.append(value) + self._transfer_out(i, value, _receiver) + + self._burnFrom(msg.sender, _burn_amount) # <---- Updates self.total_supply + + # --------------------------- Upkeep D_oracle ---------------------------- + + ma_last_time_unpacked: uint256[2] = self.unpack_2(self.ma_last_time) + last_D_packed_current: uint256 = self.last_D_packed + old_D: uint256 = last_D_packed_current & (2**128 - 1) + + self.last_D_packed = self.pack_2( + old_D - unsafe_div(old_D * _burn_amount, total_supply), # new_D = proportionally reduce D. + self._calc_moving_average( + last_D_packed_current, + self.D_ma_time, + ma_last_time_unpacked[1] + ) + ) + + if ma_last_time_unpacked[1] < block.timestamp: + ma_last_time_unpacked[1] = block.timestamp + self.ma_last_time = self.pack_2(ma_last_time_unpacked[0], ma_last_time_unpacked[1]) + + # ------------------------------- Log event ------------------------------ + + log RemoveLiquidity( + msg.sender, + amounts, + empty(DynArray[uint256, MAX_COINS]), + unsafe_sub(total_supply, _burn_amount) + ) + + # ------- Withdraw admin fees if _claim_admin_fees is set to True -------- + if _claim_admin_fees: + self._withdraw_admin_fees() + + return amounts + + +@external +@nonreentrant('lock') +def withdraw_admin_fees(): + """ + @notice Claim admin fees. Callable by anyone. + """ + self._withdraw_admin_fees() + + +# ------------------------ AMM Internal Functions ---------------------------- + + +@view +@internal +def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256) -> uint256: + + _offpeg_fee_multiplier: uint256 = self.offpeg_fee_multiplier + if _offpeg_fee_multiplier <= FEE_DENOMINATOR: + return _fee + + xps2: uint256 = (xpi + xpj) ** 2 + return unsafe_div( + unsafe_mul(_offpeg_fee_multiplier, _fee), + unsafe_add( + unsafe_sub(_offpeg_fee_multiplier, FEE_DENOMINATOR) * 4 * xpi * xpj / xps2, + FEE_DENOMINATOR + ) + ) + + +@internal +def __exchange( + x: uint256, + _xp: DynArray[uint256, MAX_COINS], + rates: DynArray[uint256, MAX_COINS], + i: int128, + j: int128, +) -> uint256: + + amp: uint256 = self._A() + D: uint256 = self.get_D(_xp, amp) + y: uint256 = self.get_y(i, j, x, _xp, amp, D) + + dy: uint256 = _xp[j] - y - 1 # -1 just in case there were some rounding errors + dy_fee: uint256 = unsafe_div( + dy * self._dynamic_fee( + unsafe_div(_xp[i] + x, 2), unsafe_div(_xp[j] + y, 2), self.fee + ), + FEE_DENOMINATOR + ) + + # Convert all to real units + dy = (dy - dy_fee) * PRECISION / rates[j] + + self.admin_balances[j] += unsafe_div( + unsafe_div(dy_fee * admin_fee, FEE_DENOMINATOR) * PRECISION, + rates[j] + ) + + # Calculate and store state prices: + xp: DynArray[uint256, MAX_COINS] = _xp + xp[i] = x + xp[j] = y + # D is not changed because we did not apply a fee + self.upkeep_oracles(xp, amp, D) + + return dy + + +@internal +def _exchange( + sender: address, + i: int128, + j: int128, + _dx: uint256, + _min_dy: uint256, + receiver: address, + expect_optimistic_transfer: bool +) -> uint256: + + assert i != j # dev: coin index out of range + assert _dx > 0 # dev: do not exchange 0 coins + + rates: DynArray[uint256, MAX_COINS] = self._stored_rates() + old_balances: DynArray[uint256, MAX_COINS] = self._balances() + xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, old_balances) + + # --------------------------- Do Transfer in ----------------------------- + + # `dx` is whatever the pool received after ERC20 transfer: + dx: uint256 = self._transfer_in( + i, + _dx, + sender, + expect_optimistic_transfer + ) + + # ------------------------------- Exchange ------------------------------- + + x: uint256 = xp[i] + unsafe_div(dx * rates[i], PRECISION) + dy: uint256 = self.__exchange(x, xp, rates, i, j) + assert dy >= _min_dy, "Exchange resulted in fewer coins than expected" + + # --------------------------- Do Transfer out ---------------------------- + + self._transfer_out(j, dy, receiver) + + # ------------------------------------------------------------------------ + + log TokenExchange(msg.sender, i, dx, j, dy) + + return dy + + +@internal +def _withdraw_admin_fees(): + fee_receiver: address = factory.fee_receiver() + if fee_receiver == empty(address): + return # Do nothing. + + admin_balances: DynArray[uint256, MAX_COINS] = self.admin_balances + for i in range(N_COINS_128, bound=MAX_COINS_128): + + if admin_balances[i] > 0: + + self._transfer_out(i, admin_balances[i], fee_receiver) + admin_balances[i] = 0 + + self.admin_balances = admin_balances + + +# --------------------------- AMM Math Functions ----------------------------- + + +@view +@internal +def get_y( + i: int128, + j: int128, + x: uint256, + xp: DynArray[uint256, MAX_COINS], + _amp: uint256, + _D: uint256 +) -> uint256: + """ + Calculate x[j] if one makes x[i] = x + + Done by solving quadratic equation iteratively. + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + assert i != j # dev: same coin + assert j >= 0 # dev: j below zero + assert j < N_COINS_128 # dev: j above N_COINS + + # should be unreachable, but good for safety + assert i >= 0 + assert i < N_COINS_128 + + amp: uint256 = _amp + D: uint256 = _D + + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + c: uint256 = D + Ann: uint256 = amp * N_COINS + + for _i in range(MAX_COINS_128): + + if _i == N_COINS_128: + break + + if _i == i: + _x = x + elif _i != j: + _x = xp[_i] + else: + continue + + S_ += _x + c = c * D / (_x * N_COINS) + + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann # - D + y: uint256 = D + + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@pure +@internal +def get_D(_xp: DynArray[uint256, MAX_COINS], _amp: uint256) -> uint256: + """ + D invariant calculation in non-overflowing integer operations + iteratively + + A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) + + Converging solution: + D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1) + """ + S: uint256 = 0 + for x in _xp: + S += x + if S == 0: + return 0 + + D: uint256 = S + Ann: uint256 = _amp * N_COINS + + for i in range(255): + + D_P: uint256 = D + for x in _xp: + D_P = D_P * D / x + D_P /= pow_mod256(N_COINS, N_COINS) + Dprev: uint256 = D + + # (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) + D = ( + (unsafe_div(Ann * S, A_PRECISION) + D_P * N_COINS) * D + / + ( + unsafe_div((Ann - A_PRECISION) * D, A_PRECISION) + + unsafe_add(N_COINS, 1) * D_P + ) + ) + + # Equality with the precision of 1 + if D > Dprev: + if D - Dprev <= 1: + return D + else: + if Dprev - D <= 1: + return D + # convergence typically occurs in 4 rounds or less, this should be unreachable! + # if it does happen the pool is borked and LPs can withdraw via `remove_liquidity` + raise + + +@pure +@internal +def get_y_D( + A: uint256, + i: int128, + xp: DynArray[uint256, MAX_COINS], + D: uint256 +) -> uint256: + """ + Calculate x[i] if one reduces D from being calculated for xp to D + + Done by solving quadratic equation iteratively. + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + assert i >= 0 # dev: i below zero + assert i < N_COINS_128 # dev: i above N_COINS + + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + c: uint256 = D + Ann: uint256 = A * N_COINS + + for _i in range(MAX_COINS_128): + + if _i == N_COINS_128: + break + + if _i != i: + _x = xp[_i] + else: + continue + S_ += _x + c = c * D / (_x * N_COINS) + + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann + y: uint256 = D + + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@view +@internal +def _A() -> uint256: + """ + Handle ramping A up or down + """ + t1: uint256 = self.future_A_time + A1: uint256 = self.future_A + + if block.timestamp < t1: + A0: uint256 = self.initial_A + t0: uint256 = self.initial_A_time + # Expressions in uint256 cannot have negative numbers, thus "if" + if A1 > A0: + return A0 + unsafe_sub(A1, A0) * (block.timestamp - t0) / (t1 - t0) + else: + return A0 - unsafe_sub(A0, A1) * (block.timestamp - t0) / (t1 - t0) + + else: # when t1 == 0 or block.timestamp >= t1 + return A1 + + +@pure +@internal +def _xp_mem( + _rates: DynArray[uint256, MAX_COINS], + _balances: DynArray[uint256, MAX_COINS] +) -> DynArray[uint256, MAX_COINS]: + + result: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + for i in range(N_COINS_128, bound=MAX_COINS_128): + result.append(unsafe_div(_rates[i] * _balances[i], PRECISION)) + return result + + +@view +@internal +def get_D_mem( + _rates: DynArray[uint256, MAX_COINS], + _balances: DynArray[uint256, MAX_COINS], + _amp: uint256 +) -> uint256: + xp: DynArray[uint256, MAX_COINS] = self._xp_mem(_rates, _balances) + return self.get_D(xp, _amp) + + +@view +@internal +def _calc_withdraw_one_coin( + _burn_amount: uint256, + i: int128 +) -> ( + uint256, + uint256, + DynArray[uint256, MAX_COINS], + uint256, + uint256 +): + # First, need to calculate + # * Get current D + # * Solve Eqn against y_i for D - _token_amount + amp: uint256 = self._A() + rates: DynArray[uint256, MAX_COINS] = self._stored_rates() + xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, self._balances()) + D0: uint256 = self.get_D(xp, amp) + + total_supply: uint256 = self.total_supply + D1: uint256 = D0 - _burn_amount * D0 / total_supply + new_y: uint256 = self.get_y_D(amp, i, xp, D1) + + base_fee: uint256 = unsafe_div( + unsafe_mul(self.fee, N_COINS), + unsafe_mul(4, unsafe_sub(N_COINS, 1)) + ) + xp_reduced: DynArray[uint256, MAX_COINS] = xp + ys: uint256 = unsafe_div((D0 + D1), unsafe_mul(2, N_COINS)) + + dx_expected: uint256 = 0 + xp_j: uint256 = 0 + xavg: uint256 = 0 + dynamic_fee: uint256 = 0 + + for j in range(MAX_COINS_128): + + if j == N_COINS_128: + break + + dx_expected = 0 + xp_j = xp[j] + + if j == i: + dx_expected = xp_j * D1 / D0 - new_y + xavg = unsafe_div((xp_j + new_y), 2) + else: + dx_expected = xp_j - xp_j * D1 / D0 + xavg = xp_j + + dynamic_fee = self._dynamic_fee(xavg, ys, base_fee) + xp_reduced[j] = xp_j - unsafe_div(dynamic_fee * dx_expected, FEE_DENOMINATOR) + + dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1) + dy_0: uint256 = (xp[i] - new_y) * PRECISION / rates[i] # w/o fees + dy = unsafe_div((dy - 1) * PRECISION, rates[i]) # Withdraw less to account for rounding errors + + # update xp with new_y for p calculations. + xp[i] = new_y + + return dy, dy_0 - dy, xp, amp, D1 + + +# -------------------------- AMM Price Methods ------------------------------- + +@pure +@internal +def pack_2(p1: uint256, p2: uint256) -> uint256: + assert p1 < 2**128 + assert p2 < 2**128 + return p1 | (p2 << 128) + + +@pure +@internal +def unpack_2(packed: uint256) -> uint256[2]: + return [packed & (2**128 - 1), packed >> 128] + + +@internal +@pure +def _get_p( + xp: DynArray[uint256, MAX_COINS], + amp: uint256, + D: uint256, +) -> DynArray[uint256, MAX_COINS]: + + # dx_0 / dx_1 only, however can have any number of coins in pool + ANN: uint256 = unsafe_mul(amp, N_COINS) + Dr: uint256 = unsafe_div(D, pow_mod256(N_COINS, N_COINS)) + + for i in range(N_COINS_128, bound=MAX_COINS_128): + Dr = Dr * D / xp[i] + + p: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + xp0_A: uint256 = unsafe_div(ANN * xp[0], A_PRECISION) + + for i in range(1, MAX_COINS): + + if i == N_COINS: + break + + p.append(10**18 * (xp0_A + unsafe_div(Dr * xp[0], xp[i])) / (xp0_A + Dr)) + + return p + + +@internal +def upkeep_oracles(xp: DynArray[uint256, MAX_COINS], amp: uint256, D: uint256): + """ + @notice Upkeeps price and D oracles. + """ + ma_last_time_unpacked: uint256[2] = self.unpack_2(self.ma_last_time) + last_prices_packed_current: DynArray[uint256, MAX_COINS] = self.last_prices_packed + last_prices_packed_new: DynArray[uint256, MAX_COINS] = last_prices_packed_current + + spot_price: DynArray[uint256, MAX_COINS] = self._get_p(xp, amp, D) + + # -------------------------- Upkeep price oracle ------------------------- + + for i in range(MAX_COINS): + + if i == N_COINS - 1: + break + + if spot_price[i] != 0: + + # Update packed prices ----------------- + last_prices_packed_new[i] = self.pack_2( + min(spot_price[i], 2 * 10**18), # <----- Cap spot value by 2. + self._calc_moving_average( + last_prices_packed_current[i], + self.ma_exp_time, + ma_last_time_unpacked[0], # index 0 is ma_last_time for prices + ) + ) + + self.last_prices_packed = last_prices_packed_new + + # ---------------------------- Upkeep D oracle --------------------------- + + last_D_packed_current: uint256 = self.last_D_packed + self.last_D_packed = self.pack_2( + D, + self._calc_moving_average( + last_D_packed_current, + self.D_ma_time, + ma_last_time_unpacked[1], # index 1 is ma_last_time for D + ) + ) + + # Housekeeping: Update ma_last_time for p and D oracles ------------------ + for i in range(2): + if ma_last_time_unpacked[i] < block.timestamp: + ma_last_time_unpacked[i] = block.timestamp + + self.ma_last_time = self.pack_2(ma_last_time_unpacked[0], ma_last_time_unpacked[1]) + + +@internal +@view +def _calc_moving_average( + packed_value: uint256, + averaging_window: uint256, + ma_last_time: uint256 +) -> uint256: + + last_spot_value: uint256 = packed_value & (2**128 - 1) + last_ema_value: uint256 = (packed_value >> 128) + + if ma_last_time < block.timestamp: # calculate new_ema_value and return that. + alpha: uint256 = self.exp( + -convert( + unsafe_div(unsafe_mul(unsafe_sub(block.timestamp, ma_last_time), 10**18), averaging_window), int256 + ) + ) + return unsafe_div(last_spot_value * (10**18 - alpha) + last_ema_value * alpha, 10**18) + + return last_ema_value + + +@view +@external +def last_price(i: uint256) -> uint256: + return self.last_prices_packed[i] & (2**128 - 1) + + +@view +@external +def ema_price(i: uint256) -> uint256: + return (self.last_prices_packed[i] >> 128) + + +@external +@view +def get_p(i: uint256) -> uint256: + """ + @notice Returns the AMM State price of token + @dev if i = 0, it will return the state price of coin[1]. + @param i index of state price (0 for coin[1], 1 for coin[2], ...) + @return uint256 The state price quoted by the AMM for coin[i+1] + """ + amp: uint256 = self._A() + xp: DynArray[uint256, MAX_COINS] = self._xp_mem( + self._stored_rates(), self._balances() + ) + D: uint256 = self.get_D(xp, amp) + return self._get_p(xp, amp, D)[i] + + +@external +@view +@nonreentrant('lock') +def price_oracle(i: uint256) -> uint256: + return self._calc_moving_average( + self.last_prices_packed[i], + self.ma_exp_time, + self.ma_last_time & (2**128 - 1) + ) + + +@external +@view +@nonreentrant('lock') +def D_oracle() -> uint256: + return self._calc_moving_average( + self.last_D_packed, + self.D_ma_time, + self.ma_last_time >> 128 + ) + + +# ----------------------------- Math Utils ----------------------------------- + + +@internal +@pure +def exp(x: int256) -> uint256: + """ + @dev Calculates the natural exponential function of a signed integer with + a precision of 1e18. + @notice Note that this function consumes about 810 gas units. The implementation + is inspired by Remco Bloemen's implementation under the MIT license here: + https://xn--2-umb.com/22/exp-ln. + @dev This implementation is derived from Snekmate, which is authored + by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license. + https://github.com/pcaversaccio/snekmate + @param x The 32-byte variable. + @return int256 The 32-byte calculation result. + """ + value: int256 = x + + # If the result is `< 0.5`, we return zero. This happens when we have the following: + # "x <= floor(log(0.5e18) * 1e18) ~ -42e18". + if (x <= -41446531673892822313): + return empty(uint256) + + # When the result is "> (2 ** 255 - 1) / 1e18" we cannot represent it as a signed integer. + # This happens when "x >= floor(log((2 ** 255 - 1) / 1e18) * 1e18) ~ 135". + assert x < 135305999368893231589, "wad_exp overflow" + + # `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 ** 96" for higher + # intermediate precision and a binary base. This base conversion is a multiplication with + # "1e18 / 2 ** 96 = 5 ** 18 / 2 ** 78". + value = unsafe_div(x << 78, 5 ** 18) + + # Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 ** 96" by factoring out powers of two + # so that "exp(x) = exp(x') * 2 ** k", where `k` is a signer integer. Solving this gives + # "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]". + k: int256 = unsafe_add(unsafe_div(value << 96, 54916777467707473351141471128), 2 ** 95) >> 96 + value = unsafe_sub(value, unsafe_mul(k, 54916777467707473351141471128)) + + # Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic, + # we will multiply by a scaling factor later. + y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1346386616545796478920950773328), value) >> 96, 57155421227552351082224309758442) + p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94201549194550492254356042504812), y) >> 96,\ + 28719021644029726153956944680412240), value), 4385272521454847904659076985693276 << 96) + + # We leave `p` in the "2 ** 192" base so that we do not have to scale it up + # again for the division. + q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2855989394907223263936484059900), value) >> 96, 50020603652535783019961831881945) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 533845033583426703283633433725380) + q = unsafe_add(unsafe_mul(q, value) >> 96, 3604857256930695427073651918091429) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 14423608567350463180887372962807573) + q = unsafe_add(unsafe_mul(q, value) >> 96, 26449188498355588339934803723976023) + + # The polynomial `q` has no zeros in the range because all its roots are complex. + # No scaling is required, as `p` is already "2 ** 96" too large. Also, + # `r` is in the range "(0.09, 0.25) * 2**96" after the division. + r: int256 = unsafe_div(p, q) + + # To finalise the calculation, we have to multiply `r` by: + # - the scale factor "s = ~6.031367120", + # - the factor "2 ** k" from the range reduction, and + # - the factor "1e18 / 2 ** 96" for the base conversion. + # We do this all at once, with an intermediate result in "2**213" base, + # so that the final right shift always gives a positive value. + + # Note that to circumvent Vyper's safecast feature for the potentially + # negative parameter value `r`, we first convert `r` to `bytes32` and + # subsequently to `uint256`. Remember that the EVM default behaviour is + # to use two's complement representation to handle signed integers. + return unsafe_mul(convert(convert(r, bytes32), uint256), 3822833074963236453042738258902158003155416615667) >> convert(unsafe_sub(195, k), uint256) + + +# ---------------------------- ERC20 Utils ----------------------------------- + +@view +@internal +def _domain_separator() -> bytes32: + if chain.id != CACHED_CHAIN_ID: + return keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + return CACHED_DOMAIN_SEPARATOR + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + # # NOTE: vyper does not allow underflows + # # so the following subtraction would revert on insufficient balance + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + + log Transfer(_from, _to, _value) + + +@internal +def _burnFrom(_from: address, _burn_amount: uint256): + + self.total_supply -= _burn_amount + self.balanceOf[_from] -= _burn_amount + log Transfer(_from, empty(address), _burn_amount) + + +@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. + """ + self._transfer(msg.sender, _to, _value) + return True + + +@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 + """ + self._transfer(_from, _to, _value) + + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != max_value(uint256): + _new_allowance: uint256 = _allowance - _value + self.allowance[_from][msg.sender] = _new_allowance + log Approval(_from, msg.sender, _new_allowance) + + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @notice Approve the passed address to transfer the specified amount of + tokens on behalf of msg.sender + @dev Beware that changing an allowance via this method brings the risk that + someone may use both the old and new allowance by unfortunate transaction + ordering: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will transfer the funds + @param _value The amount of tokens that may be transferred + @return bool success + """ + self.allowance[msg.sender][_spender] = _value + + log Approval(msg.sender, _spender, _value) + return True + + +@external +def permit( + _owner: address, + _spender: address, + _value: uint256, + _deadline: uint256, + _v: uint8, + _r: bytes32, + _s: bytes32 +) -> bool: + """ + @notice Approves spender by owner's signature to expend owner's tokens. + See https://eips.ethereum.org/EIPS/eip-2612. + @dev Inspired by https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L753-L793 + @dev Supports smart contract wallets which implement ERC1271 + https://eips.ethereum.org/EIPS/eip-1271 + @param _owner The address which is a source of funds and has signed the Permit. + @param _spender The address which is allowed to spend the funds. + @param _value The amount of tokens to be spent. + @param _deadline The timestamp after which the Permit is no longer valid. + @param _v The bytes[64] of the valid secp256k1 signature of permit by owner + @param _r The bytes[0:32] of the valid secp256k1 signature of permit by owner + @param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner + @return True, if transaction completes successfully + """ + assert _owner != empty(address) + assert block.timestamp <= _deadline + + 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)) + ) + ) + + if _owner.is_contract: + sig: Bytes[65] = concat(_abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1)) + # reentrancy not a concern since this is a staticcall + assert ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL + else: + assert ecrecover(digest, convert(_v, uint256), convert(_r, uint256), convert(_s, uint256)) == _owner + + self.allowance[_owner][_spender] = _value + self.nonces[_owner] = unsafe_add(nonce, 1) + + log Approval(_owner, _spender, _value) + return True + + +@view +@external +def DOMAIN_SEPARATOR() -> bytes32: + """ + @notice EIP712 domain separator. + @return bytes32 Domain Separator set for the current chain. + """ + return self._domain_separator() + + +# ------------------------- AMM View Functions ------------------------------- + + +@view +@external +def get_dx(i: int128, j: int128, dy: uint256) -> uint256: + """ + @notice Calculate the current input dx given output dy + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param dy Amount of `j` being received after exchange + @return Amount of `i` predicted + """ + return StableSwapViews(factory.views_implementation()).get_dx(i, j, dy, self) + + +@view +@external +def get_dy(i: int128, j: int128, dx: uint256) -> uint256: + """ + @notice Calculate the current output dy given input dx + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index value of the coin to receive + @param dx Amount of `i` being exchanged + @return Amount of `j` predicted + """ + return StableSwapViews(factory.views_implementation()).get_dy(i, j, dx, self) + + +@view +@external +def calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256: + """ + @notice Calculate the amount received when withdrawing a single coin + @param _burn_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @return Amount of coin received + """ + return self._calc_withdraw_one_coin(_burn_amount, i)[0] + + +@view +@external +@nonreentrant('lock') +def totalSupply() -> uint256: + """ + @notice The total supply of pool LP tokens + @return self.total_supply, 18 decimals. + """ + return self.total_supply + + +@view +@external +@nonreentrant('lock') +def get_virtual_price() -> uint256: + """ + @notice The current virtual price of the pool LP token + @dev Useful for calculating profits. + The method may be vulnerable to donation-style attacks if implementation + contains rebasing tokens. For integrators, caution is advised. + @return LP token virtual price normalized to 1e18 + """ + amp: uint256 = self._A() + xp: DynArray[uint256, MAX_COINS] = self._xp_mem( + self._stored_rates(), self._balances() + ) + D: uint256 = self.get_D(xp, amp) + # D is in the units similar to DAI (e.g. converted to precision 1e18) + # When balanced, D = n * x_u - total virtual value of the portfolio + return D * PRECISION / self.total_supply + + +@view +@external +def calc_token_amount( + _amounts: DynArray[uint256, MAX_COINS], + _is_deposit: bool +) -> uint256: + """ + @notice Calculate addition or reduction in token supply from a deposit or withdrawal + @param _amounts Amount of each coin being deposited + @param _is_deposit set True for deposits, False for withdrawals + @return Expected amount of LP tokens received + """ + return StableSwapViews(factory.views_implementation()).calc_token_amount(_amounts, _is_deposit, self) + + +@view +@external +def A() -> uint256: + return unsafe_div(self._A(), A_PRECISION) + + +@view +@external +def A_precise() -> uint256: + return self._A() + + +@view +@external +def balances(i: uint256) -> uint256: + """ + @notice Get the current balance of a coin within the + pool, less the accrued admin fees + @param i Index value for the coin to query balance of + @return Token balance + """ + return self._balances()[i] + + +@view +@external +def get_balances() -> DynArray[uint256, MAX_COINS]: + return self._balances() + + +@view +@external +def stored_rates() -> DynArray[uint256, MAX_COINS]: + return self._stored_rates() + + +@view +@external +def dynamic_fee(i: int128, j: int128) -> uint256: + """ + @notice Return the fee for swapping between `i` and `j` + @param i Index value for the coin to send + @param j Index value of the coin to receive + @return Swap fee expressed as an integer with 1e10 precision + """ + return StableSwapViews(factory.views_implementation()).dynamic_fee(i, j, self) + + +# --------------------------- AMM Admin Functions ---------------------------- + + +@external +def ramp_A(_future_A: uint256, _future_time: uint256): + assert msg.sender == factory.admin() # dev: only owner + assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME + assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time + + _initial_A: uint256 = self._A() + _future_A_p: uint256 = _future_A * A_PRECISION + + assert _future_A > 0 and _future_A < MAX_A + if _future_A_p < _initial_A: + assert _future_A_p * MAX_A_CHANGE >= _initial_A + else: + assert _future_A_p <= _initial_A * MAX_A_CHANGE + + self.initial_A = _initial_A + self.future_A = _future_A_p + self.initial_A_time = block.timestamp + self.future_A_time = _future_time + + log RampA(_initial_A, _future_A_p, block.timestamp, _future_time) + + +@external +def stop_ramp_A(): + assert msg.sender == factory.admin() # dev: only owner + + current_A: uint256 = self._A() + self.initial_A = current_A + self.future_A = current_A + self.initial_A_time = block.timestamp + self.future_A_time = block.timestamp + # now (block.timestamp < t1) is always False, so we return saved A + + log StopRampA(current_A, block.timestamp) + + +@external +def set_new_fee(_new_fee: uint256, _new_offpeg_fee_multiplier: uint256): + + assert msg.sender == factory.admin() + + # set new fee: + assert _new_fee <= MAX_FEE + self.fee = _new_fee + + # set new offpeg_fee_multiplier: + assert _new_offpeg_fee_multiplier * _new_fee <= MAX_FEE * FEE_DENOMINATOR # dev: offpeg multiplier exceeds maximum + self.offpeg_fee_multiplier = _new_offpeg_fee_multiplier + + log ApplyNewFee(_new_fee, _new_offpeg_fee_multiplier) + + +@external +def set_ma_exp_time(_ma_exp_time: uint256, _D_ma_time: uint256): + """ + @notice Set the moving average window of the price oracles. + @param _ma_exp_time Moving average window for the price oracle. It is time_in_seconds / ln(2). + @param _D_ma_time Moving average window for the D oracle. It is time_in_seconds / ln(2). + """ + assert msg.sender == factory.admin() # dev: only owner + assert unsafe_mul(_ma_exp_time, _D_ma_time) > 0 # dev: 0 in input values + + self.ma_exp_time = _ma_exp_time + self.D_ma_time = _D_ma_time + + log SetNewMATime(_ma_exp_time, _D_ma_time) diff --git a/contracts/amms/stableswapng/CurveStableSwapNGMath.vy b/contracts/amms/stableswapng/CurveStableSwapNGMath.vy new file mode 100644 index 0000000..25acc73 --- /dev/null +++ b/contracts/amms/stableswapng/CurveStableSwapNGMath.vy @@ -0,0 +1,269 @@ +# pragma version 0.3.10 +# pragma optimize gas +# pragma evm-version shanghai +""" +@title CurveStableSwapNGMath +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Math for StableSwapMetaNG implementation +""" + +MAX_COINS: constant(uint256) = 8 +MAX_COINS_128: constant(int128) = 8 +A_PRECISION: constant(uint256) = 100 + + +@external +@pure +def get_y( + i: int128, + j: int128, + x: uint256, + xp: DynArray[uint256, MAX_COINS], + _amp: uint256, + _D: uint256, + _n_coins: uint256 +) -> uint256: + """ + Calculate x[j] if one makes x[i] = x + + Done by solving quadratic equation iteratively. + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + n_coins_128: int128 = convert(_n_coins, int128) + + assert i != j # dev: same coin + assert j >= 0 # dev: j below zero + assert j < n_coins_128 # dev: j above N_COINS + + # should be unreachable, but good for safety + assert i >= 0 + assert i < n_coins_128 + + amp: uint256 = _amp + D: uint256 = _D + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + c: uint256 = D + Ann: uint256 = amp * _n_coins + + for _i in range(MAX_COINS_128): + + if _i == n_coins_128: + break + + if _i == i: + _x = x + elif _i != j: + _x = xp[_i] + else: + continue + + S_ += _x + c = c * D / (_x * _n_coins) + + c = c * D * A_PRECISION / (Ann * _n_coins) + b: uint256 = S_ + D * A_PRECISION / Ann # - D + y: uint256 = D + + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@external +@pure +def get_D( + _xp: DynArray[uint256, MAX_COINS], + _amp: uint256, + _n_coins: uint256 +) -> uint256: + """ + D invariant calculation in non-overflowing integer operations + iteratively + + A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) + + Converging solution: + D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1) + """ + S: uint256 = 0 + for x in _xp: + S += x + if S == 0: + return 0 + + D: uint256 = S + Ann: uint256 = _amp * _n_coins + + for i in range(255): + + D_P: uint256 = D + for x in _xp: + D_P = D_P * D / x # If division by 0, this will be borked: only withdrawal will work. And that is good + D_P /= pow_mod256(_n_coins, _n_coins) + Dprev: uint256 = D + + # (Ann * S / A_PRECISION + D_P * _n_coins) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (_n_coins + 1) * D_P) + D = ( + (unsafe_div(Ann * S, A_PRECISION) + D_P * _n_coins) * + D / ( + unsafe_div((Ann - A_PRECISION) * D, A_PRECISION) + + unsafe_add(_n_coins, 1) * D_P + ) + ) + # Equality with the precision of 1 + if D > Dprev: + if D - Dprev <= 1: + return D + else: + if Dprev - D <= 1: + return D + # convergence typically occurs in 4 rounds or less, this should be unreachable! + # if it does happen the pool is borked and LPs can withdraw via `remove_liquidity` + raise + + +@external +@pure +def get_y_D( + A: uint256, + i: int128, + xp: DynArray[uint256, MAX_COINS], + D: uint256, + _n_coins: uint256 +) -> uint256: + """ + Calculate x[i] if one reduces D from being calculated for xp to D + + Done by solving quadratic equation iteratively. + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + n_coins_128: int128 = convert(_n_coins, int128) + + assert i >= 0 # dev: i below zero + assert i < n_coins_128 # dev: i above N_COINS + + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + c: uint256 = D + Ann: uint256 = A * _n_coins + + for _i in range(MAX_COINS_128): + + if _i == n_coins_128: + break + + if _i != i: + _x = xp[_i] + else: + continue + S_ += _x + c = c * D / (_x * _n_coins) + + c = c * D * A_PRECISION / (Ann * _n_coins) + b: uint256 = S_ + D * A_PRECISION / Ann + y: uint256 = D + + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@external +@pure +def exp(x: int256) -> uint256: + + """ + @dev Calculates the natural exponential function of a signed integer with + a precision of 1e18. + @notice Note that this function consumes about 810 gas units. The implementation + is inspired by Remco Bloemen's implementation under the MIT license here: + https://xn--2-umb.com/22/exp-ln. + @dev This implementation is derived from Snekmate, which is authored + by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license. + https://github.com/pcaversaccio/snekmate + @param x The 32-byte variable. + @return int256 The 32-byte calculation result. + """ + value: int256 = x + + # If the result is `< 0.5`, we return zero. This happens when we have the following: + # "x <= floor(log(0.5e18) * 1e18) ~ -42e18". + if (x <= -41446531673892822313): + return empty(uint256) + + # When the result is "> (2 ** 255 - 1) / 1e18" we cannot represent it as a signed integer. + # This happens when "x >= floor(log((2 ** 255 - 1) / 1e18) * 1e18) ~ 135". + assert x < 135305999368893231589, "wad_exp overflow" + + # `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 ** 96" for higher + # intermediate precision and a binary base. This base conversion is a multiplication with + # "1e18 / 2 ** 96 = 5 ** 18 / 2 ** 78". + value = unsafe_div(x << 78, 5 ** 18) + + # Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 ** 96" by factoring out powers of two + # so that "exp(x) = exp(x') * 2 ** k", where `k` is a signer integer. Solving this gives + # "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]". + k: int256 = unsafe_add(unsafe_div(value << 96, 54916777467707473351141471128), 2 ** 95) >> 96 + value = unsafe_sub(value, unsafe_mul(k, 54916777467707473351141471128)) + + # Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic, + # we will multiply by a scaling factor later. + y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1346386616545796478920950773328), value) >> 96, 57155421227552351082224309758442) + p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94201549194550492254356042504812), y) >> 96,\ + 28719021644029726153956944680412240), value), 4385272521454847904659076985693276 << 96) + + # We leave `p` in the "2 ** 192" base so that we do not have to scale it up + # again for the division. + q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2855989394907223263936484059900), value) >> 96, 50020603652535783019961831881945) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 533845033583426703283633433725380) + q = unsafe_add(unsafe_mul(q, value) >> 96, 3604857256930695427073651918091429) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 14423608567350463180887372962807573) + q = unsafe_add(unsafe_mul(q, value) >> 96, 26449188498355588339934803723976023) + + # The polynomial `q` has no zeros in the range because all its roots are complex. + # No scaling is required, as `p` is already "2 ** 96" too large. Also, + # `r` is in the range "(0.09, 0.25) * 2**96" after the division. + r: int256 = unsafe_div(p, q) + + # To finalise the calculation, we have to multiply `r` by: + # - the scale factor "s = ~6.031367120", + # - the factor "2 ** k" from the range reduction, and + # - the factor "1e18 / 2 ** 96" for the base conversion. + # We do this all at once, with an intermediate result in "2**213" base, + # so that the final right shift always gives a positive value. + + # Note that to circumvent Vyper's safecast feature for the potentially + # negative parameter value `r`, we first convert `r` to `bytes32` and + # subsequently to `uint256`. Remember that the EVM default behaviour is + # to use two's complement representation to handle signed integers. + return unsafe_mul(convert(convert(r, bytes32), uint256), 3822833074963236453042738258902158003155416615667) >> convert(unsafe_sub(195, k), uint256) diff --git a/contracts/amms/stableswapng/CurveStableSwapNGViews.vy b/contracts/amms/stableswapng/CurveStableSwapNGViews.vy new file mode 100644 index 0000000..6ff702d --- /dev/null +++ b/contracts/amms/stableswapng/CurveStableSwapNGViews.vy @@ -0,0 +1,671 @@ +# pragma version 0.3.10 +# pragma evm-version shanghai +""" +@title CurveStableSwapNGViews +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Auxiliary contract for Stableswap-NG containing utility methods for + integrators +""" + +interface StableSwapNG: + def N_COINS() -> uint256: view + def BASE_POOL() -> address: view + def BASE_N_COINS() -> uint256: view + def stored_rates() -> DynArray[uint256, MAX_COINS]: view + def balances(i: uint256) -> uint256: view + def get_balances() -> DynArray[uint256, MAX_COINS]: view + def fee() -> uint256: view + def get_dy(i: int128, j: int128, dx: uint256) -> uint256: view + def A() -> uint256: view + def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: view + def totalSupply() -> uint256: view + def calc_token_amount(amounts: DynArray[uint256, MAX_COINS], deposit: bool) -> uint256: view + def offpeg_fee_multiplier() -> uint256: view + +interface StableSwap2: + def calc_token_amount(amounts: uint256[2], deposit: bool) -> uint256: view + +interface StableSwap3: + def calc_token_amount(amounts: uint256[3], deposit: bool) -> uint256: view + + +A_PRECISION: constant(uint256) = 100 +MAX_COINS: constant(uint256) = 8 +PRECISION: constant(uint256) = 10 ** 18 +FEE_DENOMINATOR: constant(uint256) = 10 ** 10 + + +# ------------------------------ Public Getters ------------------------------ + + +@view +@external +def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256: + """ + @notice Calculate the current input dx given output dy + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index valie of the coin to recieve + @param dy Amount of `j` being received after exchange + @return Amount of `i` predicted + """ + N_COINS: uint256 = StableSwapNG(pool).N_COINS() + return self._get_dx(i, j, dy, pool, False, N_COINS) + + +@view +@external +def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256: + """ + @notice Calculate the current output dy given input dx + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index valie of the coin to recieve + @param dx Amount of `i` being exchanged + @return Amount of `j` predicted + """ + N_COINS: uint256 = StableSwapNG(pool).N_COINS() + + rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS) + + amp: uint256 = StableSwapNG(pool).A() * A_PRECISION + D: uint256 = self.get_D(xp, amp, N_COINS) + + x: uint256 = xp[i] + (dx * rates[i] / PRECISION) + y: uint256 = self.get_y(i, j, x, xp, amp, D, N_COINS) + dy: uint256 = xp[j] - y - 1 + + base_fee: uint256 = StableSwapNG(pool).fee() + fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier() + fee: uint256 = self._dynamic_fee((xp[i] + x) / 2, (xp[j] + y) / 2, base_fee, fee_multiplier) * dy / FEE_DENOMINATOR + + return (dy - fee) * PRECISION / rates[j] + + +@view +@external +def get_dx_underlying( + i: int128, + j: int128, + dy: uint256, + pool: address, +) -> uint256: + + BASE_POOL: address = StableSwapNG(pool).BASE_POOL() + BASE_N_COINS: uint256 = StableSwapNG(pool).BASE_N_COINS() + N_COINS: uint256 = StableSwapNG(pool).N_COINS() + base_pool_has_static_fee: bool = self._has_static_fee(BASE_POOL) + + # CASE 1: Swap does not involve Metapool at all. In this case, we kindly as the user + # to use the right pool for their swaps. + if min(i, j) > 0: + raise "Not a Metapool Swap. Use Base pool." + + # CASE 2: + # 1. meta token_0 of (unknown amount) > base pool lp_token + # 2. base pool lp_token > calc_withdraw_one_coin gives dy amount of (j-1)th base coin + # So, need to do the following calculations: + # 1. calc_token_amounts on base pool for depositing liquidity on (j-1)th token > lp_tokens. + # 2. get_dx on metapool for i = 0, and j = 1 (base lp token) with amt calculated in (1). + if i == 0: + # Calculate LP tokens that are burnt to receive dy amount of base_j tokens. + lp_amount_burnt: uint256 = self._base_calc_token_amount( + dy, j - 1, BASE_N_COINS, BASE_POOL, False + ) + return self._get_dx(0, 1, lp_amount_burnt, pool, False, N_COINS) + + # CASE 3: Swap in token i-1 from base pool and swap out dy amount of token 0 (j) from metapool. + # 1. deposit i-1 token from base pool > receive base pool lp_token + # 2. swap base pool lp token > 0th token of the metapool + # So, need to do the following calculations: + # 1. get_dx on metapool with i = 0, j = 1 > gives how many base lp tokens are required for receiving + # dy amounts of i-1 tokens from the metapool + # 2. We have number of lp tokens: how many i-1 base pool coins are needed to mint that many tokens? + # We don't have a method where user inputs lp tokens and it gives number of coins of (i-1)th token + # is needed to mint that many base_lp_tokens. Instead, we will use calc_withdraw_one_coin. That's + # close enough. + lp_amount_required: uint256 = self._get_dx(1, 0, dy, pool, False, N_COINS) + return StableSwapNG(BASE_POOL).calc_withdraw_one_coin(lp_amount_required, i-1) + + +@view +@external +def get_dy_underlying( + i: int128, + j: int128, + dx: uint256, + pool: address, +) -> uint256: + """ + @notice Calculate the current output dy given input dx on underlying + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index valie of the coin to recieve + @param dx Amount of `i` being exchanged + @return Amount of `j` predicted + """ + + N_COINS: uint256 = StableSwapNG(pool).N_COINS() + MAX_COIN: int128 = convert(N_COINS, int128) - 1 + BASE_POOL: address = StableSwapNG(pool).BASE_POOL() + + rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS) + + x: uint256 = 0 + base_i: int128 = 0 + base_j: int128 = 0 + meta_i: int128 = 0 + meta_j: int128 = 0 + + if i != 0: + base_i = i - MAX_COIN + meta_i = 1 + if j != 0: + base_j = j - MAX_COIN + meta_j = 1 + + if i == 0: + + x = xp[i] + dx * rates[0] / 10**18 + + else: + + if j == 0: + + # i is from BasePool + base_n_coins: uint256 = StableSwapNG(pool).BASE_N_COINS() + x = self._base_calc_token_amount( + dx, base_i, base_n_coins, BASE_POOL, True + ) * rates[1] / PRECISION + + # Adding number of pool tokens + x += xp[1] + + else: + # If both are from the base pool + return StableSwapNG(BASE_POOL).get_dy(base_i, base_j, dx) + + # This pool is involved only when in-pool assets are used + amp: uint256 = StableSwapNG(pool).A() * A_PRECISION + D: uint256 = self.get_D(xp, amp, N_COINS) + y: uint256 = self.get_y(meta_i, meta_j, x, xp, amp, D, N_COINS) + dy: uint256 = xp[meta_j] - y - 1 + + # calculate output after subtracting dynamic fee + base_fee: uint256 = StableSwapNG(pool).fee() + fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier() + + dynamic_fee: uint256 = self._dynamic_fee((xp[meta_i] + x) / 2, (xp[meta_j] + y) / 2, base_fee, fee_multiplier) + dy = (dy - dynamic_fee * dy / FEE_DENOMINATOR) + + # If output is going via the metapool + if j == 0: + dy = dy * 10**18 / rates[0] + else: + # j is from BasePool + # The fee is already accounted for + dy = StableSwapNG(BASE_POOL).calc_withdraw_one_coin(dy * PRECISION / rates[1], base_j) + + return dy + + +@view +@external +def calc_token_amount( + _amounts: DynArray[uint256, MAX_COINS], + _is_deposit: bool, + pool: address +) -> uint256: + """ + @notice Calculate addition or reduction in token supply from a deposit or withdrawal + @param _amounts Amount of each coin being deposited + @param _is_deposit set True for deposits, False for withdrawals + @return Expected amount of LP tokens received + """ + amp: uint256 = StableSwapNG(pool).A() * A_PRECISION + N_COINS: uint256 = StableSwapNG(pool).N_COINS() + + rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + old_balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + rates, old_balances, xp = self._get_rates_balances_xp(pool, N_COINS) + + # Initial invariant + D0: uint256 = self.get_D(xp, amp, N_COINS) + + total_supply: uint256 = StableSwapNG(pool).totalSupply() + new_balances: DynArray[uint256, MAX_COINS] = old_balances + for i in range(MAX_COINS): + if i == N_COINS: + break + + amount: uint256 = _amounts[i] + if _is_deposit: + new_balances[i] += amount + else: + new_balances[i] -= amount + + # Invariant after change + for idx in range(MAX_COINS): + if idx == N_COINS: + break + xp[idx] = rates[idx] * new_balances[idx] / PRECISION + D1: uint256 = self.get_D(xp, amp, N_COINS) + + # We need to recalculate the invariant accounting for fees + # to calculate fair user's share + D2: uint256 = D1 + if total_supply > 0: + + # Only account for fees if we are not the first to deposit + base_fee: uint256 = StableSwapNG(pool).fee() * N_COINS / (4 * (N_COINS - 1)) + fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier() + _dynamic_fee_i: uint256 = 0 + xs: uint256 = 0 + ys: uint256 = (D0 + D1) / N_COINS + + for i in range(MAX_COINS): + if i == N_COINS: + break + + ideal_balance: uint256 = D1 * old_balances[i] / D0 + difference: uint256 = 0 + new_balance: uint256 = new_balances[i] + if ideal_balance > new_balance: + difference = ideal_balance - new_balance + else: + difference = new_balance - ideal_balance + + xs = old_balances[i] + new_balance + _dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee, fee_multiplier) + new_balances[i] -= _dynamic_fee_i * difference / FEE_DENOMINATOR + + for idx in range(MAX_COINS): + if idx == N_COINS: + break + xp[idx] = rates[idx] * new_balances[idx] / PRECISION + + D2 = self.get_D(xp, amp, N_COINS) + else: + return D1 # Take the dust if there was any + + diff: uint256 = 0 + if _is_deposit: + diff = D2 - D0 + else: + diff = D0 - D2 + return diff * total_supply / D0 + + +@view +@external +def calc_withdraw_one_coin(_burn_amount: uint256, i: int128, pool: address) -> uint256: + # First, need to calculate + # * Get current D + # * Solve Eqn against y_i for D - _token_amount + + amp: uint256 = StableSwapNG(pool).A() * A_PRECISION + N_COINS: uint256 = StableSwapNG(pool).N_COINS() + + rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS) + + D0: uint256 = self.get_D(xp, amp, N_COINS) + + total_supply: uint256 = StableSwapNG(pool).totalSupply() + D1: uint256 = D0 - _burn_amount * D0 / total_supply + new_y: uint256 = self.get_y_D(amp, i, xp, D1, N_COINS) + ys: uint256 = (D0 + D1) / (2 * N_COINS) + + base_fee: uint256 = StableSwapNG(pool).fee() * N_COINS / (4 * (N_COINS - 1)) + fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier() + xp_reduced: DynArray[uint256, MAX_COINS] = xp + xp_j: uint256 = 0 + xavg: uint256 = 0 + dynamic_fee: uint256 = 0 + + for j in range(MAX_COINS): + + if j == N_COINS: + break + + dx_expected: uint256 = 0 + xp_j = xp[j] + if convert(j, int128) == i: + dx_expected = xp_j * D1 / D0 - new_y + xavg = (xp[j] + new_y) / 2 + else: + dx_expected = xp_j - xp_j * D1 / D0 + xavg = xp[j] + + dynamic_fee = self._dynamic_fee(xavg, ys, base_fee, fee_multiplier) + xp_reduced[j] = xp_j - dynamic_fee * dx_expected / FEE_DENOMINATOR + + dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1, N_COINS) + dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors + + return dy + + +@view +@external +def dynamic_fee(i: int128, j: int128, pool:address) -> uint256: + """ + @notice Return the fee for swapping between `i` and `j` + @param i Index value for the coin to send + @param j Index value of the coin to recieve + @return Swap fee expressed as an integer with 1e10 precision + """ + N_COINS: uint256 = StableSwapNG(pool).N_COINS() + fee: uint256 = StableSwapNG(pool).fee() + fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier() + + rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS) + + return self._dynamic_fee(xp[i], xp[j], fee, fee_multiplier) + + +# ----------------------------- Utility Methods ------------------------------ + + +@view +@internal +def _has_static_fee(pool: address) -> bool: + + success: bool = False + response: Bytes[32] = b"" + success, response = raw_call( + pool, + concat( + method_id("dynamic_fee(int128,int128)"), + convert(1, bytes32), + convert(0, bytes32) + ), + max_outsize=32, + revert_on_failure=False, + is_static_call=True + ) + + return success + + +@view +@internal +def _get_dx( + i: int128, + j: int128, + dy: uint256, + pool: address, + static_fee: bool, + N_COINS: uint256 +) -> uint256: + + rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS) + + amp: uint256 = StableSwapNG(pool).A() * A_PRECISION + D: uint256 = self.get_D(xp, amp, N_COINS) + + base_fee: uint256 = StableSwapNG(pool).fee() + dy_with_fee: uint256 = dy * rates[j] / PRECISION + 1 + + fee: uint256 = base_fee + if not static_fee: + fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier() + fee = self._dynamic_fee(xp[i], xp[j], base_fee, fee_multiplier) + + y: uint256 = xp[j] - dy_with_fee * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee) + x: uint256 = self.get_y(j, i, y, xp, amp, D, N_COINS) + return (x - xp[i]) * PRECISION / rates[i] + + +@view +@internal +def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256, _fee_multiplier: uint256) -> uint256: + + if _fee_multiplier <= FEE_DENOMINATOR: + return _fee + + xps2: uint256 = (xpi + xpj) ** 2 + return ( + (_fee_multiplier * _fee) / + ((_fee_multiplier - FEE_DENOMINATOR) * 4 * xpi * xpj / xps2 + FEE_DENOMINATOR) + ) + + +@internal +@view +def _base_calc_token_amount( + dx: uint256, + base_i: int128, + base_n_coins: uint256, + base_pool: address, + is_deposit: bool +) -> uint256: + + if base_n_coins == 2: + + base_inputs: uint256[2] = empty(uint256[2]) + base_inputs[base_i] = dx + return StableSwap2(base_pool).calc_token_amount(base_inputs, is_deposit) + + elif base_n_coins == 3: + + base_inputs: uint256[3] = empty(uint256[3]) + base_inputs[base_i] = dx + return StableSwap3(base_pool).calc_token_amount(base_inputs, is_deposit) + + else: + + base_inputs: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + for i in range(base_n_coins, bound=MAX_COINS): + if i == convert(base_i, uint256): + base_inputs.append(dx) + else: + base_inputs.append(0) + return StableSwapNG(base_pool).calc_token_amount(base_inputs, is_deposit) + + +@internal +@pure +def newton_y(b: uint256, c: uint256, D: uint256, _y: uint256) -> uint256: + + y_prev: uint256 = 0 + y: uint256 = _y + + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@view +@internal +def get_y( + i: int128, + j: int128, + x: uint256, + xp: DynArray[uint256, MAX_COINS], + _amp: uint256, + _D: uint256, + N_COINS: uint256 +) -> uint256: + """ + Calculate x[j] if one makes x[i] = x + + Done by solving quadratic equation iteratively. + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + assert i != j # dev: same coin + assert j >= 0 # dev: j below zero + assert j < convert(N_COINS, int128) # dev: j above N_COINS + + # should be unreachable, but good for safety + assert i >= 0 + assert i < convert(N_COINS, int128) + + amp: uint256 = _amp + D: uint256 = _D + S_: uint256 = 0 + _x: uint256 = 0 + c: uint256 = D + Ann: uint256 = amp * N_COINS + + for _i in range(MAX_COINS): + + if _i == N_COINS: + break + + if convert(_i, int128) == i: + _x = x + elif convert(_i, int128) != j: + _x = xp[_i] + else: + continue + S_ += _x + c = c * D / (_x * N_COINS) + + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann # - D + y: uint256 = D + + return self.newton_y(b, c, D, y) + + +@pure +@internal +def get_D(_xp: DynArray[uint256, MAX_COINS], _amp: uint256, N_COINS: uint256) -> uint256: + """ + D invariant calculation in non-overflowing integer operations + iteratively + + A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) + + Converging solution: + D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1) + """ + S: uint256 = 0 + for i in range(MAX_COINS): + if i == N_COINS: + break + S += _xp[i] + + if S == 0: + return 0 + + D: uint256 = S + Ann: uint256 = _amp * N_COINS + + for i in range(255): + + D_P: uint256 = D + for x in _xp: + D_P = D_P * D / x + D_P /= pow_mod256(N_COINS, N_COINS) + Dprev: uint256 = D + + D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) + # Equality with the precision of 1 + if D > Dprev: + if D - Dprev <= 1: + return D + else: + if Dprev - D <= 1: + return D + # convergence typically occurs in 4 rounds or less, this should be unreachable! + # if it does happen the pool is borked and LPs can withdraw via `remove_liquidity` + raise + + +@pure +@internal +def get_y_D( + A: uint256, + i: int128, + xp: DynArray[uint256, MAX_COINS], + D: uint256, + N_COINS: uint256 +) -> uint256: + """ + Calculate x[i] if one reduces D from being calculated for xp to D + + Done by solving quadratic equation iteratively. + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + N_COINS_128: int128 = convert(N_COINS, int128) + assert i >= 0 # dev: i below zero + assert i < N_COINS_128 # dev: i above N_COINS + + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + c: uint256 = D + Ann: uint256 = A * N_COINS + + for _i in range(MAX_COINS): + + if _i == N_COINS: + break + + if _i != convert(i, uint256): + _x = xp[_i] + else: + continue + S_ += _x + c = c * D / (_x * N_COINS) + + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann + y: uint256 = D + + return self.newton_y(b, c, D, y) + + +@view +@internal +def _get_rates_balances_xp(pool: address, N_COINS: uint256) -> ( + DynArray[uint256, MAX_COINS], + DynArray[uint256, MAX_COINS], + DynArray[uint256, MAX_COINS], +): + + rates: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).stored_rates() + balances: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).get_balances() + xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + for idx in range(MAX_COINS): + if idx == N_COINS: + break + xp.append(rates[idx] * balances[idx] / PRECISION) + + return rates, balances, xp diff --git a/contracts/amms/tricryptong/CurveCryptoMathOptimized3.vy b/contracts/amms/tricryptong/CurveCryptoMathOptimized3.vy new file mode 100644 index 0000000..695c794 --- /dev/null +++ b/contracts/amms/tricryptong/CurveCryptoMathOptimized3.vy @@ -0,0 +1,880 @@ +# pragma version 0.3.10 +# pragma optimize gas +# pragma evm-version paris + +# (c) Curve.Fi, 2020-2023 +# AMM Math for 3-coin Curve Cryptoswap Pools +# +# Unless otherwise agreed on, only contracts owned by Curve DAO or +# Swiss Stake GmbH are allowed to call this contract. + +""" +@title CurveTricryptoMathOptimized +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Curve AMM Math for 3 unpegged assets (e.g. ETH, BTC, USD). +""" + +N_COINS: constant(uint256) = 3 +A_MULTIPLIER: constant(uint256) = 10000 + +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA: constant(uint256) = 5 * 10**16 + +MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 100 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 + +version: public(constant(String[8])) = "v2.0.0" + + +# ------------------------ AMM math functions -------------------------------- + + +@external +@view +def get_y( + _ANN: uint256, _gamma: uint256, x: uint256[N_COINS], _D: uint256, i: uint256 +) -> uint256[2]: + """ + @notice Calculate x[i] given other balances x[0..N_COINS-1] and invariant D. + @dev ANN = A * N**N. + @param _ANN AMM.A() value. + @param _gamma AMM.gamma() value. + @param x Balances multiplied by prices and precisions of all coins. + @param _D Invariant. + @param i Index of coin to calculate y. + """ + + # Safety checks + assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A + assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma + assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D + + frac: uint256 = 0 + for k in range(3): + if k != i: + frac = x[k] * 10**18 / _D + assert frac > 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]" + # if above conditions are met, x[k] > 0 + + j: uint256 = 0 + k: uint256 = 0 + if i == 0: + j = 1 + k = 2 + elif i == 1: + j = 0 + k = 2 + elif i == 2: + j = 0 + k = 1 + + ANN: int256 = convert(_ANN, int256) + gamma: int256 = convert(_gamma, int256) + D: int256 = convert(_D, int256) + x_j: int256 = convert(x[j], int256) + x_k: int256 = convert(x[k], int256) + gamma2: int256 = unsafe_mul(gamma, gamma) + + a: int256 = 10**36 / 27 + + # 10**36/9 + 2*10**18*gamma/27 - D**2/x_j*gamma**2*ANN/27**2/convert(A_MULTIPLIER, int256)/x_k + b: int256 = ( + unsafe_add( + 10**36 / 9, + unsafe_div(unsafe_mul(2 * 10**18, gamma), 27) + ) + - unsafe_div( + unsafe_div( + unsafe_div( + unsafe_mul( + unsafe_div(unsafe_mul(D, D), x_j), + gamma2 + ) * ANN, + 27**2 + ), + convert(A_MULTIPLIER, int256) + ), + x_k, + ) + ) # <------- The first two expressions can be unsafe, and unsafely added. + + # 10**36/9 + gamma*(gamma + 4*10**18)/27 + gamma**2*(x_j+x_k-D)/D*ANN/27/convert(A_MULTIPLIER, int256) + c: int256 = ( + unsafe_add( + 10**36 / 9, + unsafe_div(unsafe_mul(gamma, unsafe_add(gamma, 4 * 10**18)), 27) + ) + + unsafe_div( + unsafe_div( + unsafe_mul( + unsafe_div(gamma2 * unsafe_sub(unsafe_add(x_j, x_k), D), D), + ANN + ), + 27 + ), + convert(A_MULTIPLIER, int256), + ) + ) # <--------- Same as above with the first two expressions. In the third + # expression, x_j + x_k will not overflow since we know their range from + # previous assert statements. + + # (10**18 + gamma)**2/27 + d: int256 = unsafe_div(unsafe_add(10**18, gamma)**2, 27) + + # abs(3*a*c/b - b) + d0: int256 = abs(unsafe_mul(3, a) * c / b - b) # <------------ a is smol. + + divider: int256 = 0 + if d0 > 10**48: + divider = 10**30 + elif d0 > 10**44: + divider = 10**26 + elif d0 > 10**40: + divider = 10**22 + elif d0 > 10**36: + divider = 10**18 + elif d0 > 10**32: + divider = 10**14 + elif d0 > 10**28: + divider = 10**10 + elif d0 > 10**24: + divider = 10**6 + elif d0 > 10**20: + divider = 10**2 + else: + divider = 1 + + additional_prec: int256 = 0 + if abs(a) > abs(b): + additional_prec = abs(unsafe_div(a, b)) + a = unsafe_div(unsafe_mul(a, additional_prec), divider) + b = unsafe_div(b * additional_prec, divider) + c = unsafe_div(c * additional_prec, divider) + d = unsafe_div(d * additional_prec, divider) + else: + additional_prec = abs(unsafe_div(b, a)) + a = unsafe_div(a / additional_prec, divider) + b = unsafe_div(unsafe_div(b, additional_prec), divider) + c = unsafe_div(unsafe_div(c, additional_prec), divider) + d = unsafe_div(unsafe_div(d, additional_prec), divider) + + # 3*a*c/b - b + _3ac: int256 = unsafe_mul(3, a) * c + delta0: int256 = unsafe_div(_3ac, b) - b + + # 9*a*c/b - 2*b - 27*a**2/b*d/b + delta1: int256 = ( + unsafe_div(3 * _3ac, b) + - unsafe_mul(2, b) + - unsafe_div(unsafe_div(27 * a**2, b) * d, b) + ) + + # delta1**2 + 4*delta0**2/b*delta0 + sqrt_arg: int256 = ( + delta1**2 + + unsafe_div(4 * delta0**2, b) * delta0 + ) + + sqrt_val: int256 = 0 + if sqrt_arg > 0: + sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256) + else: + return [self._newton_y(_ANN, _gamma, x, _D, i), 0] + + b_cbrt: int256 = 0 + if b >= 0: + b_cbrt = convert(self._cbrt(convert(b, uint256)), int256) + else: + b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256) + + second_cbrt: int256 = 0 + if delta1 > 0: + # convert(self._cbrt(convert((delta1 + sqrt_val), uint256)/2), int256) + second_cbrt = convert( + self._cbrt(unsafe_div(convert(delta1 + sqrt_val, uint256), 2)), + int256 + ) + else: + second_cbrt = -convert( + self._cbrt(unsafe_div(convert(-(delta1 - sqrt_val), uint256), 2)), + int256 + ) + + # b_cbrt*b_cbrt/10**18*second_cbrt/10**18 + C1: int256 = unsafe_div( + unsafe_div(b_cbrt * b_cbrt, 10**18) * second_cbrt, + 10**18 + ) + + # (b + b*delta0/C1 - C1)/3 + root_K0: int256 = unsafe_div(b + b * delta0 / C1 - C1, 3) + + # D*D/27/x_k*D/x_j*root_K0/a + root: int256 = unsafe_div( + unsafe_div( + unsafe_div(unsafe_div(D * D, 27), x_k) * D, + x_j + ) * root_K0, + a + ) + + out: uint256[2] = [ + convert(root, uint256), + convert(unsafe_div(10**18 * root_K0, a), uint256) + ] + + frac = unsafe_div(out[0] * 10**18, _D) + assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe value for y" + # due to precision issues, get_y can be off by 2 wei or so wrt _newton_y + + return out + + +@internal +@view +def _newton_y( + ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256 +) -> uint256: + + # Calculate x[i] given A, gamma, xp and D using newton's method. + # This is the original method; get_y replaces it, but defaults to + # this version conditionally. + + # We can ignore safuty checks since they are already done in get_y + + frac: uint256 = 0 + for k in range(3): + if k != i: + frac = x[k] * 10**18 / D + assert frac > 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]" + + y: uint256 = D / N_COINS + K0_i: uint256 = 10**18 + S_i: uint256 = 0 + + x_sorted: uint256[N_COINS] = x + x_sorted[i] = 0 + x_sorted = self._sort(x_sorted) # From high to low + + convergence_limit: uint256 = max(max(x_sorted[0] / 10**14, D / 10**14), 100) + + for j in range(2, N_COINS + 1): + _x: uint256 = x_sorted[N_COINS - j] + y = y * D / (_x * N_COINS) # Small _x first + S_i += _x + + for j in range(N_COINS - 1): + K0_i = K0_i * x_sorted[j] * N_COINS / D # Large _x first + + # initialise variables: + diff: uint256 = 0 + y_prev: uint256 = 0 + K0: uint256 = 0 + S: uint256 = 0 + _g1k0: uint256 = 0 + mul1: uint256 = 0 + mul2: uint256 = 0 + yfprime: uint256 = 0 + _dyfprime: uint256 = 0 + fprime: uint256 = 0 + y_minus: uint256 = 0 + y_plus: uint256 = 0 + + for j in range(255): + y_prev = y + + K0 = K0_i * y * N_COINS / D + S = S_i + y + + _g1k0 = gamma + 10**18 + if _g1k0 > K0: + _g1k0 = _g1k0 - K0 + 1 + else: + _g1k0 = K0 - _g1k0 + 1 + + # mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + + # 2*K0 / _g1k0 + mul2 = 10**18 + (2 * 10**18) * K0 / _g1k0 + + yfprime = 10**18 * y + S * mul2 + mul1 + _dyfprime = D * mul2 + if yfprime < _dyfprime: + y = y_prev / 2 + continue + else: + yfprime -= _dyfprime + + fprime = yfprime / y + + # y -= f / f_prime; y = (y * fprime - f) / fprime + y_minus = mul1 / fprime + y_plus = (yfprime + 10**18 * D) / fprime + y_minus * 10**18 / K0 + y_minus += 10**18 * S / fprime + + if y_plus < y_minus: + y = y_prev / 2 + else: + y = y_plus - y_minus + + if y > y_prev: + diff = y - y_prev + else: + diff = y_prev - y + + if diff < max(convergence_limit, y / 10**14): + frac = y * 10**18 / D + assert frac > 10**16 - 1 and frac < 10**20 + 1, "Unsafe value for y" + return y + + raise "Did not converge" + + +@external +@view +def newton_D( + ANN: uint256, + gamma: uint256, + x_unsorted: uint256[N_COINS], + K0_prev: uint256 = 0, +) -> uint256: + """ + @notice Finding the invariant via newtons method using good initial guesses. + @dev ANN is higher by the factor A_MULTIPLIER + @dev ANN is already A * N**N + @param ANN the A * N**N value + @param gamma the gamma value + @param x_unsorted the array of coin balances (not sorted) + @param K0_prev apriori for newton's method derived from get_y_int. Defaults + to zero (no apriori) + """ + x: uint256[N_COINS] = self._sort(x_unsorted) + assert x[0] < max_value(uint256) / 10**18 * N_COINS**N_COINS # dev: out of limits + assert x[0] > 0 # dev: empty pool + + # Safe to do unsafe add since we checked largest x's bounds previously + S: uint256 = unsafe_add(unsafe_add(x[0], x[1]), x[2]) + D: uint256 = 0 + + if K0_prev == 0: + # Geometric mean of 3 numbers cannot be larger than the largest number + # so the following is safe to do: + D = unsafe_mul(N_COINS, self._geometric_mean(x)) + else: + if S > 10**36: + D = self._cbrt( + unsafe_div( + unsafe_div(x[0] * x[1], 10**36) * x[2], + K0_prev + ) * 27 * 10**12 + ) + elif S > 10**24: + D = self._cbrt( + unsafe_div( + unsafe_div(x[0] * x[1], 10**24) * x[2], + K0_prev + ) * 27 * 10**6 + ) + else: + D = self._cbrt( + unsafe_div( + unsafe_div(x[0] * x[1], 10**18) * x[2], + K0_prev + ) * 27 + ) + + # D not zero here if K0_prev > 0, and we checked if x[0] is gt 0. + + # initialise variables: + K0: uint256 = 0 + _g1k0: uint256 = 0 + mul1: uint256 = 0 + mul2: uint256 = 0 + neg_fprime: uint256 = 0 + D_plus: uint256 = 0 + D_minus: uint256 = 0 + D_prev: uint256 = 0 + + diff: uint256 = 0 + frac: uint256 = 0 + + for i in range(255): + + D_prev = D + + # K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D + K0 = unsafe_div( + unsafe_mul( + unsafe_mul( + unsafe_div( + unsafe_mul( + unsafe_mul( + unsafe_div( + unsafe_mul( + unsafe_mul(10**18, x[0]), N_COINS + ), + D, + ), + x[1], + ), + N_COINS, + ), + D, + ), + x[2], + ), + N_COINS, + ), + D, + ) # <-------- We can convert the entire expression using unsafe math. + # since x_i is not too far from D, so overflow is not expected. Also + # D > 0, since we proved that already. unsafe_div is safe. K0 > 0 + # since we can safely assume that D < 10**18 * x[0]. K0 is also + # in the range of 10**18 (it's a property). + + _g1k0 = unsafe_add(gamma, 10**18) # <--------- safe to do unsafe_add. + + if _g1k0 > K0: # The following operations can safely be unsafe. + _g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) + else: + _g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) + + # D / (A * N**N) * _g1k0**2 / gamma**2 + # mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN + mul1 = unsafe_div( + unsafe_mul( + unsafe_mul( + unsafe_div( + unsafe_mul( + unsafe_div(unsafe_mul(10**18, D), gamma), _g1k0 + ), + gamma, + ), + _g1k0, + ), + A_MULTIPLIER, + ), + ANN, + ) # <------ Since D > 0, gamma is small, _g1k0 is small, the rest are + # non-zero and small constants, and D has a cap in this method, + # we can safely convert everything to unsafe maths. + + # 2*N*K0 / _g1k0 + # mul2 = (2 * 10**18) * N_COINS * K0 / _g1k0 + mul2 = unsafe_div( + unsafe_mul(2 * 10**18 * N_COINS, K0), _g1k0 + ) # <--------------- K0 is approximately around D, which has a cap of + # 10**15 * 10**18 + 1, since we get that in get_y which is called + # with newton_D. _g1k0 > 0, so the entire expression can be unsafe. + + # neg_fprime: uint256 = (S + S * mul2 / 10**18) + mul1 * N_COINS / K0 - mul2 * D / 10**18 + neg_fprime = unsafe_sub( + unsafe_add( + unsafe_add(S, unsafe_div(unsafe_mul(S, mul2), 10**18)), + unsafe_div(unsafe_mul(mul1, N_COINS), K0), + ), + unsafe_div(unsafe_mul(mul2, D), 10**18), + ) # <--- mul1 is a big number but not huge: safe to unsafely multiply + # with N_coins. neg_fprime > 0 if this expression executes. + # mul2 is in the range of 10**18, since K0 is in that range, S * mul2 + # is safe. The first three sums can be done using unsafe math safely + # and since the final expression will be small since mul2 is small, we + # can safely do the entire expression unsafely. + + # D -= f / fprime + # D * (neg_fprime + S) / neg_fprime + D_plus = unsafe_div(D * unsafe_add(neg_fprime, S), neg_fprime) + + # D*D / neg_fprime + D_minus = unsafe_div(D * D, neg_fprime) + + # Since we know K0 > 0, and neg_fprime > 0, several unsafe operations + # are possible in the following. Also, (10**18 - K0) is safe to mul. + # So the only expressions we keep safe are (D_minus + ...) and (D * ...) + if 10**18 > K0: + # D_minus += D * (mul1 / neg_fprime) / 10**18 * (10**18 - K0) / K0 + D_minus += unsafe_div( + unsafe_mul( + unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18), + unsafe_sub(10**18, K0), + ), + K0, + ) + else: + # D_minus -= D * (mul1 / neg_fprime) / 10**18 * (K0 - 10**18) / K0 + D_minus -= unsafe_div( + unsafe_mul( + unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18), + unsafe_sub(K0, 10**18), + ), + K0, + ) + + if D_plus > D_minus: + D = unsafe_sub(D_plus, D_minus) # <--------- Safe since we check. + else: + D = unsafe_div(unsafe_sub(D_minus, D_plus), 2) + + if D > D_prev: + diff = unsafe_sub(D, D_prev) + else: + diff = unsafe_sub(D_prev, D) + + # Could reduce precision for gas efficiency here: + if unsafe_mul(diff, 10**14) < max(10**16, D): + + # Test that we are safe with the next get_y + for _x in x: + frac = unsafe_div(unsafe_mul(_x, 10**18), D) + assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]" + + return D + raise "Did not converge" + + +@external +@view +def get_p( + _xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[N_COINS-1] +) -> uint256[N_COINS-1]: + """ + @notice Calculates dx/dy. + @dev Output needs to be multiplied with price_scale to get the actual value. + @param _xp Balances of the pool. + @param _D Current value of D. + @param _A_gamma Amplification coefficient and gamma. + """ + + assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe D values + + # K0 = P * N**N / D**N. + # K0 is dimensionless and has 10**36 precision: + K0: uint256 = unsafe_div( + unsafe_div(unsafe_div(27 * _xp[0] * _xp[1], _D) * _xp[2], _D) * 10**36, + _D + ) + + # GK0 is in 10**36 precision and is dimensionless. + # GK0 = ( + # 2 * _K0 * _K0 / 10**36 * _K0 / 10**36 + # + (gamma + 10**18)**2 + # - (_K0 * _K0 / 10**36 * (2 * gamma + 3 * 10**18) / 10**18) + # ) + # GK0 is always positive. So the following should never revert: + GK0: uint256 = ( + unsafe_div(unsafe_div(2 * K0 * K0, 10**36) * K0, 10**36) + + pow_mod256(unsafe_add(_A_gamma[1], 10**18), 2) + - unsafe_div( + unsafe_div(pow_mod256(K0, 2), 10**36) * unsafe_add(unsafe_mul(2, _A_gamma[1]), 3 * 10**18), + 10**18 + ) + ) + + # NNAG2 = N**N * A * gamma**2 + NNAG2: uint256 = unsafe_div(unsafe_mul(_A_gamma[0], pow_mod256(_A_gamma[1], 2)), A_MULTIPLIER) + + # denominator = (GK0 + NNAG2 * x / D * _K0 / 10**36) + denominator: uint256 = (GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[0], _D) * K0, 10**36) ) + + # p_xy = x * (GK0 + NNAG2 * y / D * K0 / 10**36) / y * 10**18 / denominator + # p_xz = x * (GK0 + NNAG2 * z / D * K0 / 10**36) / z * 10**18 / denominator + # p is in 10**18 precision. + return [ + unsafe_div( + _xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[1], _D) * K0, 10**36) ) / _xp[1] * 10**18, + denominator + ), + unsafe_div( + _xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[2], _D) * K0, 10**36) ) / _xp[2] * 10**18, + denominator + ), + ] + + +# --------------------------- Math Utils ------------------------------------- + + +@external +@view +def cbrt(x: uint256) -> uint256: + """ + @notice Calculate the cubic root of a number in 1e18 precision + @dev Consumes around 1500 gas units + @param x The number to calculate the cubic root of + """ + return self._cbrt(x) + + +@external +@view +def geometric_mean(_x: uint256[3]) -> uint256: + """ + @notice Calculate the geometric mean of a list of numbers in 1e18 precision. + @param _x list of 3 numbers to sort + """ + return self._geometric_mean(_x) + + +@external +@view +def reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256: + """ + @notice Calculates the reduction coefficient for the given x and fee_gamma + @dev This method is used for calculating fees. + @param x The x values + @param fee_gamma The fee gamma value + """ + return self._reduction_coefficient(x, fee_gamma) + + +@external +@view +def wad_exp(_power: int256) -> uint256: + """ + @notice Calculates the e**x with 1e18 precision + @param _power The number to calculate the exponential of + """ + return self._snekmate_wad_exp(_power) + + +@internal +@pure +def _reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256: + + # fee_gamma / (fee_gamma + (1 - K)) + # where + # K = prod(x) / (sum(x) / N)**N + # (all normalized to 1e18) + + S: uint256 = x[0] + x[1] + x[2] + + # Could be good to pre-sort x, but it is used only for dynamic fee + K: uint256 = 10**18 * N_COINS * x[0] / S + K = unsafe_div(K * N_COINS * x[1], S) # <- unsafe div is safu. + K = unsafe_div(K * N_COINS * x[2], S) + + if fee_gamma > 0: + K = fee_gamma * 10**18 / (fee_gamma + 10**18 - K) + + return K + + +@internal +@pure +def _snekmate_wad_exp(x: int256) -> uint256: + + """ + @dev Calculates the natural exponential function of a signed integer with + a precision of 1e18. + @notice Note that this function consumes about 810 gas units. The implementation + is inspired by Remco Bloemen's implementation under the MIT license here: + https://xn--2-umb.com/22/exp-ln. + @dev This implementation is derived from Snekmate, which is authored + by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license. + https://github.com/pcaversaccio/snekmate + @param x The 32-byte variable. + @return int256 The 32-byte calculation result. + """ + value: int256 = x + + # If the result is `< 0.5`, we return zero. This happens when we have the following: + # "x <= floor(log(0.5e18) * 1e18) ~ -42e18". + if (x <= -42139678854452767551): + return empty(uint256) + + # When the result is "> (2 ** 255 - 1) / 1e18" we cannot represent it as a signed integer. + # This happens when "x >= floor(log((2 ** 255 - 1) / 1e18) * 1e18) ~ 135". + assert x < 135305999368893231589, "wad_exp overflow" + + # `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 ** 96" for higher + # intermediate precision and a binary base. This base conversion is a multiplication with + # "1e18 / 2 ** 96 = 5 ** 18 / 2 ** 78". + value = unsafe_div(x << 78, 5 ** 18) + + # Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 ** 96" by factoring out powers of two + # so that "exp(x) = exp(x') * 2 ** k", where `k` is a signer integer. Solving this gives + # "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]". + k: int256 = unsafe_add(unsafe_div(value << 96, 54916777467707473351141471128), 2 ** 95) >> 96 + value = unsafe_sub(value, unsafe_mul(k, 54916777467707473351141471128)) + + # Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic, + # we will multiply by a scaling factor later. + y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1346386616545796478920950773328), value) >> 96, 57155421227552351082224309758442) + p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94201549194550492254356042504812), y) >> 96,\ + 28719021644029726153956944680412240), value), 4385272521454847904659076985693276 << 96) + + # We leave `p` in the "2 ** 192" base so that we do not have to scale it up + # again for the division. + q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2855989394907223263936484059900), value) >> 96, 50020603652535783019961831881945) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 533845033583426703283633433725380) + q = unsafe_add(unsafe_mul(q, value) >> 96, 3604857256930695427073651918091429) + q = unsafe_sub(unsafe_mul(q, value) >> 96, 14423608567350463180887372962807573) + q = unsafe_add(unsafe_mul(q, value) >> 96, 26449188498355588339934803723976023) + + # The polynomial `q` has no zeros in the range because all its roots are complex. + # No scaling is required, as `p` is already "2 ** 96" too large. Also, + # `r` is in the range "(0.09, 0.25) * 2**96" after the division. + r: int256 = unsafe_div(p, q) + + # To finalise the calculation, we have to multiply `r` by: + # - the scale factor "s = ~6.031367120", + # - the factor "2 ** k" from the range reduction, and + # - the factor "1e18 / 2 ** 96" for the base conversion. + # We do this all at once, with an intermediate result in "2**213" base, + # so that the final right shift always gives a positive value. + + # Note that to circumvent Vyper's safecast feature for the potentially + # negative parameter value `r`, we first convert `r` to `bytes32` and + # subsequently to `uint256`. Remember that the EVM default behaviour is + # to use two's complement representation to handle signed integers. + return unsafe_mul(convert(convert(r, bytes32), uint256), 3822833074963236453042738258902158003155416615667) >> convert(unsafe_sub(195, k), uint256) + + +@internal +@pure +def _snekmate_log_2(x: uint256, roundup: bool) -> uint256: + """ + @notice An `internal` helper function that returns the log in base 2 + of `x`, following the selected rounding direction. + @dev This implementation is derived from Snekmate, which is authored + by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license. + https://github.com/pcaversaccio/snekmate + @dev Note that it returns 0 if given 0. The implementation is + inspired by OpenZeppelin's implementation here: + https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol. + @param x The 32-byte variable. + @param roundup The Boolean variable that specifies whether + to round up or not. The default `False` is round down. + @return uint256 The 32-byte calculation result. + """ + value: uint256 = x + result: uint256 = empty(uint256) + + # The following lines cannot overflow because we have the well-known + # decay behaviour of `log_2(max_value(uint256)) < max_value(uint256)`. + if x >> 128 != empty(uint256): + value = x >> 128 + result = 128 + if value >> 64 != empty(uint256): + value = value >> 64 + result = unsafe_add(result, 64) + if value >> 32 != empty(uint256): + value = value >> 32 + result = unsafe_add(result, 32) + if value >> 16 != empty(uint256): + value = value >> 16 + result = unsafe_add(result, 16) + if value >> 8 != empty(uint256): + value = value >> 8 + result = unsafe_add(result, 8) + if value >> 4 != empty(uint256): + value = value >> 4 + result = unsafe_add(result, 4) + if value >> 2 != empty(uint256): + value = value >> 2 + result = unsafe_add(result, 2) + if value >> 1 != empty(uint256): + result = unsafe_add(result, 1) + + if (roundup and (1 << result) < x): + result = unsafe_add(result, 1) + + return result + + +@internal +@pure +def _cbrt(x: uint256) -> uint256: + + xx: uint256 = 0 + if x >= 115792089237316195423570985008687907853269 * 10**18: + xx = x + elif x >= 115792089237316195423570985008687907853269: + xx = unsafe_mul(x, 10**18) + else: + xx = unsafe_mul(x, 10**36) + + log2x: int256 = convert(self._snekmate_log_2(xx, False), int256) + + # When we divide log2x by 3, the remainder is (log2x % 3). + # So if we just multiply 2**(log2x/3) and discard the remainder to calculate our + # guess, the newton method will need more iterations to converge to a solution, + # since it is missing that precision. It's a few more calculations now to do less + # calculations later: + # pow = log2(x) // 3 + # remainder = log2(x) % 3 + # initial_guess = 2 ** pow * cbrt(2) ** remainder + # substituting -> 2 = 1.26 ≈ 1260 / 1000, we get: + # + # initial_guess = 2 ** pow * 1260 ** remainder // 1000 ** remainder + + remainder: uint256 = convert(log2x, uint256) % 3 + a: uint256 = unsafe_div( + unsafe_mul( + pow_mod256(2, unsafe_div(convert(log2x, uint256), 3)), # <- pow + pow_mod256(1260, remainder), + ), + pow_mod256(1000, remainder), + ) + + # Because we chose good initial values for cube roots, 7 newton raphson iterations + # are just about sufficient. 6 iterations would result in non-convergences, and 8 + # would be one too many iterations. Without initial values, the iteration count + # can go up to 20 or greater. The iterations are unrolled. This reduces gas costs + # but takes up more bytecode: + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3) + + if x >= 115792089237316195423570985008687907853269 * 10**18: + a = unsafe_mul(a, 10**12) + elif x >= 115792089237316195423570985008687907853269: + a = unsafe_mul(a, 10**6) + + return a + + +@internal +@pure +def _sort(unsorted_x: uint256[3]) -> uint256[3]: + + # Sorts a three-array number in a descending order: + + x: uint256[N_COINS] = unsorted_x + temp_var: uint256 = x[0] + if x[0] < x[1]: + x[0] = x[1] + x[1] = temp_var + if x[0] < x[2]: + temp_var = x[0] + x[0] = x[2] + x[2] = temp_var + if x[1] < x[2]: + temp_var = x[1] + x[1] = x[2] + x[2] = temp_var + + return x + + +@internal +@view +def _geometric_mean(_x: uint256[3]) -> uint256: + + # calculates a geometric mean for three numbers. + + prod: uint256 = unsafe_div( + unsafe_div(_x[0] * _x[1], 10**18) * _x[2], + 10**18 + ) + + if prod == 0: + return 0 + + return self._cbrt(prod) diff --git a/contracts/amms/tricryptong/CurveCryptoViews3Optimized.vy b/contracts/amms/tricryptong/CurveCryptoViews3Optimized.vy new file mode 100644 index 0000000..4f8e6bc --- /dev/null +++ b/contracts/amms/tricryptong/CurveCryptoViews3Optimized.vy @@ -0,0 +1,415 @@ +# pragma version 0.3.10 +# pragma optimize gas +# pragma evm-version paris +""" +@title CurveCryptoViews3Optimized +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice This contract contains view-only external methods which can be + gas-inefficient when called from smart contracts. +""" + +from vyper.interfaces import ERC20 + + +interface Curve: + def MATH() -> Math: view + def A() -> uint256: view + def gamma() -> uint256: view + def price_scale(i: uint256) -> uint256: view + def price_oracle(i: uint256) -> uint256: view + def get_virtual_price() -> uint256: view + def balances(i: uint256) -> uint256: view + def D() -> uint256: view + def fee_calc(xp: uint256[N_COINS]) -> uint256: view + def calc_token_fee( + amounts: uint256[N_COINS], xp: uint256[N_COINS] + ) -> uint256: view + def future_A_gamma_time() -> uint256: view + def totalSupply() -> uint256: view + def precisions() -> uint256[N_COINS]: view + def packed_fee_params() -> uint256: view + + +interface Math: + def newton_D( + ANN: uint256, + gamma: uint256, + x_unsorted: uint256[N_COINS], + K0_prev: uint256 + ) -> uint256: view + def get_y( + ANN: uint256, + gamma: uint256, + x: uint256[N_COINS], + D: uint256, + i: uint256, + ) -> uint256[2]: view + def cbrt(x: uint256) -> uint256: view + def reduction_coefficient( + x: uint256[N_COINS], fee_gamma: uint256 + ) -> uint256: view + + +N_COINS: constant(uint256) = 3 +PRECISION: constant(uint256) = 10**18 + + +@external +@view +def get_dy( + i: uint256, j: uint256, dx: uint256, swap: address +) -> uint256: + + dy: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + + # dy = (get_y(x + dx) - y) * (1 - fee) + dy, xp = self._get_dy_nofee(i, j, dx, swap) + dy -= Curve(swap).fee_calc(xp) * dy / 10**10 + + return dy + + +@view +@external +def get_dx( + i: uint256, j: uint256, dy: uint256, swap: address +) -> uint256: + + dx: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + fee_dy: uint256 = 0 + _dy: uint256 = dy + + # for more precise dx (but never exact), increase num loops + for k in range(5): + dx, xp = self._get_dx_fee(i, j, _dy, swap) + fee_dy = Curve(swap).fee_calc(xp) * _dy / 10**10 + _dy = dy + fee_dy + 1 + + return dx + + +@view +@external +def calc_withdraw_one_coin( + token_amount: uint256, i: uint256, swap: address +) -> uint256: + + return self._calc_withdraw_one_coin(token_amount, i, swap)[0] + + +@view +@external +def calc_token_amount( + amounts: uint256[N_COINS], deposit: bool, swap: address +) -> uint256: + + d_token: uint256 = 0 + amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + + d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap) + d_token -= ( + Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1 + ) + + return d_token + + +@external +@view +def calc_fee_get_dy(i: uint256, j: uint256, dx: uint256, swap: address +) -> uint256: + + dy: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + dy, xp = self._get_dy_nofee(i, j, dx, swap) + + return Curve(swap).fee_calc(xp) * dy / 10**10 + + +@external +@view +def calc_fee_withdraw_one_coin( + token_amount: uint256, i: uint256, swap: address +) -> uint256: + + return self._calc_withdraw_one_coin(token_amount, i, swap)[1] + + +@view +@external +def calc_fee_token_amount( + amounts: uint256[N_COINS], deposit: bool, swap: address +) -> uint256: + + d_token: uint256 = 0 + amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap) + + return Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1 + + +@internal +@view +def _calc_D_ramp( + A: uint256, + gamma: uint256, + xp: uint256[N_COINS], + precisions: uint256[N_COINS], + price_scale: uint256[N_COINS - 1], + swap: address +) -> uint256: + + math: Math = Curve(swap).MATH() + + D: uint256 = Curve(swap).D() + if Curve(swap).future_A_gamma_time() > block.timestamp: + _xp: uint256[N_COINS] = xp + _xp[0] *= precisions[0] + for k in range(N_COINS - 1): + _xp[k + 1] = ( + _xp[k + 1] * price_scale[k] * precisions[k + 1] / PRECISION + ) + D = math.newton_D(A, gamma, _xp, 0) + + return D + + +@internal +@view +def _get_dx_fee( + i: uint256, j: uint256, dy: uint256, swap: address +) -> (uint256, uint256[N_COINS]): + + # here, dy must include fees (and 1 wei offset) + + assert i != j and i < N_COINS and j < N_COINS, "coin index out of range" + assert dy > 0, "do not exchange out 0 coins" + + math: Math = Curve(swap).MATH() + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + precisions: uint256[N_COINS] = empty(uint256[N_COINS]) + price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + D: uint256 = 0 + token_supply: uint256 = 0 + A: uint256 = 0 + gamma: uint256 = 0 + + xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap) + + # adjust xp with output dy. dy contains fee element, which we handle later + # (hence this internal method is called _get_dx_fee) + xp[j] -= dy + xp[0] *= precisions[0] + for k in range(N_COINS - 1): + xp[k + 1] = xp[k + 1] * price_scale[k] * precisions[k + 1] / PRECISION + + x_out: uint256[2] = math.get_y(A, gamma, xp, D, i) + dx: uint256 = x_out[0] - xp[i] + xp[i] = x_out[0] + if i > 0: + dx = dx * PRECISION / price_scale[i - 1] + dx /= precisions[i] + + return dx, xp + + +@internal +@view +def _get_dy_nofee( + i: uint256, j: uint256, dx: uint256, swap: address +) -> (uint256, uint256[N_COINS]): + + assert i != j and i < N_COINS and j < N_COINS, "coin index out of range" + assert dx > 0, "do not exchange 0 coins" + + math: Math = Curve(swap).MATH() + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + precisions: uint256[N_COINS] = empty(uint256[N_COINS]) + price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + D: uint256 = 0 + token_supply: uint256 = 0 + A: uint256 = 0 + gamma: uint256 = 0 + + xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap) + + # adjust xp with input dx + xp[i] += dx + xp[0] *= precisions[0] + for k in range(N_COINS - 1): + xp[k + 1] = xp[k + 1] * price_scale[k] * precisions[k + 1] / PRECISION + + y_out: uint256[2] = math.get_y(A, gamma, xp, D, j) + dy: uint256 = xp[j] - y_out[0] - 1 + xp[j] = y_out[0] + if j > 0: + dy = dy * PRECISION / price_scale[j - 1] + dy /= precisions[j] + + return dy, xp + + +@internal +@view +def _calc_dtoken_nofee( + amounts: uint256[N_COINS], deposit: bool, swap: address +) -> (uint256, uint256[N_COINS], uint256[N_COINS]): + + math: Math = Curve(swap).MATH() + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + precisions: uint256[N_COINS] = empty(uint256[N_COINS]) + price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + D0: uint256 = 0 + token_supply: uint256 = 0 + A: uint256 = 0 + gamma: uint256 = 0 + + xp, D0, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap) + + amountsp: uint256[N_COINS] = amounts + if deposit: + for k in range(N_COINS): + xp[k] += amounts[k] + else: + for k in range(N_COINS): + xp[k] -= amounts[k] + + xp[0] *= precisions[0] + amountsp[0] *= precisions[0] + for k in range(N_COINS - 1): + p: uint256 = price_scale[k] * precisions[k + 1] + xp[k + 1] = xp[k + 1] * p / PRECISION + amountsp[k + 1] = amountsp[k + 1] * p / PRECISION + + D: uint256 = math.newton_D(A, gamma, xp, 0) + d_token: uint256 = token_supply * D / D0 + + if deposit: + d_token -= token_supply + else: + d_token = token_supply - d_token + + return d_token, amountsp, xp + + +@internal +@view +def _calc_withdraw_one_coin( + token_amount: uint256, + i: uint256, + swap: address +) -> (uint256, uint256): + + token_supply: uint256 = Curve(swap).totalSupply() + assert token_amount <= token_supply # dev: token amount more than supply + assert i < N_COINS # dev: coin out of range + + math: Math = Curve(swap).MATH() + + xx: uint256[N_COINS] = empty(uint256[N_COINS]) + price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + for k in range(N_COINS): + xx[k] = Curve(swap).balances(k) + if k > 0: + price_scale[k - 1] = Curve(swap).price_scale(k - 1) + + precisions: uint256[N_COINS] = Curve(swap).precisions() + A: uint256 = Curve(swap).A() + gamma: uint256 = Curve(swap).gamma() + xp: uint256[N_COINS] = precisions + D0: uint256 = 0 + p: uint256 = 0 + + price_scale_i: uint256 = PRECISION * precisions[0] + xp[0] *= xx[0] + for k in range(1, N_COINS): + + p = price_scale[k-1] + if i == k: + price_scale_i = p * xp[i] + xp[k] = xp[k] * xx[k] * p / PRECISION + + if Curve(swap).future_A_gamma_time() > block.timestamp: + D0 = math.newton_D(A, gamma, xp, 0) + else: + D0 = Curve(swap).D() + + D: uint256 = D0 + + fee: uint256 = self._fee(xp, swap) + dD: uint256 = token_amount * D / token_supply + + D_fee: uint256 = fee * dD / (2 * 10**10) + 1 + approx_fee: uint256 = N_COINS * D_fee * xx[i] / D + + D -= (dD - D_fee) + + y_out: uint256[2] = math.get_y(A, gamma, xp, D, i) + dy: uint256 = (xp[i] - y_out[0]) * PRECISION / price_scale_i + xp[i] = y_out[0] + + return dy, approx_fee + + +@internal +@view +def _fee(xp: uint256[N_COINS], swap: address) -> uint256: + math: Math = Curve(swap).MATH() + packed_fee_params: uint256 = Curve(swap).packed_fee_params() + fee_params: uint256[3] = self._unpack(packed_fee_params) + f: uint256 = math.reduction_coefficient(xp, fee_params[2]) + return (fee_params[0] * f + fee_params[1] * (10**18 - f)) / 10**18 + + +@internal +@view +def _prep_calc(swap: address) -> ( + uint256[N_COINS], + uint256, + uint256, + uint256[N_COINS-1], + uint256, + uint256, + uint256[N_COINS] +): + + precisions: uint256[N_COINS] = Curve(swap).precisions() + token_supply: uint256 = Curve(swap).totalSupply() + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + for k in range(N_COINS): + xp[k] = Curve(swap).balances(k) + + price_scale: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1]) + for k in range(N_COINS - 1): + price_scale[k] = Curve(swap).price_scale(k) + + A: uint256 = Curve(swap).A() + gamma: uint256 = Curve(swap).gamma() + D: uint256 = self._calc_D_ramp( + A, gamma, xp, precisions, price_scale, swap + ) + + return xp, D, token_supply, price_scale, A, gamma, precisions + + +@internal +@view +def _unpack(_packed: uint256) -> uint256[3]: + """ + @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) + @param val The uint256 to unpack + @return The unpacked uint256[3] + """ + return [ + (_packed >> 128) & 18446744073709551615, + (_packed >> 64) & 18446744073709551615, + _packed & 18446744073709551615, + ] diff --git a/contracts/amms/tricryptong/CurveL2TricryptoFactory.vy b/contracts/amms/tricryptong/CurveL2TricryptoFactory.vy new file mode 100644 index 0000000..517f1c9 --- /dev/null +++ b/contracts/amms/tricryptong/CurveL2TricryptoFactory.vy @@ -0,0 +1,462 @@ +# pragma version 0.3.10 +# pragma optimize gas +# pragma evm-version paris +""" +@title CurveL2TricryptoFactory +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Permissionless 3-coin cryptoswap pool deployer and registry +""" + +interface TricryptoPool: + def balances(i: uint256) -> uint256: view + +interface ERC20: + def decimals() -> uint256: view + + +event TricryptoPoolDeployed: + pool: address + name: String[64] + symbol: String[32] + weth: address + coins: address[N_COINS] + math: address + salt: bytes32 + packed_precisions: uint256 + packed_A_gamma: uint256 + packed_fee_params: uint256 + packed_rebalancing_params: uint256 + packed_prices: uint256 + deployer: address + +event UpdateFeeReceiver: + _old_fee_receiver: address + _new_fee_receiver: address + +event UpdatePoolImplementation: + _implemention_id: uint256 + _old_pool_implementation: address + _new_pool_implementation: address + +event UpdateMathImplementation: + _old_math_implementation: address + _new_math_implementation: address + +event UpdateViewsImplementation: + _old_views_implementation: address + _new_views_implementation: address + +event TransferOwnership: + _old_owner: address + _new_owner: address + + +struct PoolArray: + coins: address[N_COINS] + decimals: uint256[N_COINS] + implementation: address + + +N_COINS: constant(uint256) = 3 +A_MULTIPLIER: constant(uint256) = 10000 + +# Limits +MAX_FEE: constant(uint256) = 10 * 10 ** 9 + +MIN_GAMMA: constant(uint256) = 10 ** 10 +MAX_GAMMA: constant(uint256) = 5 * 10**16 + +MIN_A: constant(uint256) = N_COINS ** N_COINS * A_MULTIPLIER / 100 +MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS + +PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1) +PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1 + +admin: public(address) +future_admin: public(address) + +# fee receiver for all pools: +fee_receiver: public(address) + +pool_implementations: public(HashMap[uint256, address]) +views_implementation: public(address) +math_implementation: public(address) + +# mapping of coins -> pools for trading +# a mapping key is generated for each pair of addresses via +# `bitwise_xor(convert(a, uint256), convert(b, uint256))` +markets: HashMap[uint256, address[4294967296]] +market_counts: HashMap[uint256, uint256] + +pool_count: public(uint256) # actual length of pool_list +pool_data: HashMap[address, PoolArray] +pool_list: public(address[4294967296]) # master list of pools + + +@external +def __init__(_fee_receiver: address, _admin: address): + + self.fee_receiver = _fee_receiver + self.admin = _admin + + log UpdateFeeReceiver(empty(address), _fee_receiver) + log TransferOwnership(empty(address), _admin) + + +@internal +@view +def _pack(x: uint256[3]) -> uint256: + """ + @notice Packs 3 integers with values <= 10**18 into a uint256 + @param x The uint256[3] to pack + @return The packed uint256 + """ + return (x[0] << 128) | (x[1] << 64) | x[2] + + + +# <--- Pool Deployers ---> + +@external +def deploy_pool( + _name: String[64], + _symbol: String[32], + _coins: address[N_COINS], + _weth: address, + implementation_id: uint256, + A: uint256, + gamma: uint256, + mid_fee: uint256, + out_fee: uint256, + fee_gamma: uint256, + allowed_extra_profit: uint256, + adjustment_step: uint256, + ma_exp_time: uint256, + initial_prices: uint256[N_COINS-1], +) -> address: + """ + @notice Deploy a new pool + @param _name Name of the new plain pool + @param _symbol Symbol for the new plain pool - will be concatenated with factory symbol + + @return Address of the deployed pool + """ + pool_implementation: address = self.pool_implementations[implementation_id] + assert pool_implementation != empty(address), "Pool implementation not set" + + # Validate parameters + assert A > MIN_A-1 + assert A < MAX_A+1 + + assert gamma > MIN_GAMMA-1 + assert gamma < MAX_GAMMA+1 + + assert mid_fee < MAX_FEE-1 # mid_fee can be zero + assert out_fee >= mid_fee + assert out_fee < MAX_FEE-1 + assert fee_gamma < 10**18+1 + assert fee_gamma > 0 + + assert allowed_extra_profit < 10**18+1 + + assert adjustment_step < 10**18+1 + assert adjustment_step > 0 + + assert ma_exp_time < 872542 # 7 * 24 * 60 * 60 / ln(2) + assert ma_exp_time > 86 # 60 / ln(2) + + assert min(initial_prices[0], initial_prices[1]) > 10**6 + assert max(initial_prices[0], initial_prices[1]) < 10**30 + + assert _coins[0] != _coins[1] and _coins[1] != _coins[2] and _coins[0] != _coins[2], "Duplicate coins" + + decimals: uint256[N_COINS] = empty(uint256[N_COINS]) + precisions: uint256[N_COINS] = empty(uint256[N_COINS]) + for i in range(N_COINS): + d: uint256 = ERC20(_coins[i]).decimals() + assert d < 19, "Max 18 decimals for coins" + decimals[i] = d + precisions[i] = 10** (18 - d) + + # pack precisions + packed_precisions: uint256 = self._pack(precisions) + + # pack fees + packed_fee_params: uint256 = self._pack( + [mid_fee, out_fee, fee_gamma] + ) + + # pack liquidity rebalancing params + packed_rebalancing_params: uint256 = self._pack( + [allowed_extra_profit, adjustment_step, ma_exp_time] + ) + + # pack A_gamma + packed_A_gamma: uint256 = A << 128 + packed_A_gamma = packed_A_gamma | gamma + + # pack initial prices + packed_prices: uint256 = 0 + for k in range(N_COINS - 1): + packed_prices = packed_prices << PRICE_SIZE + p: uint256 = initial_prices[N_COINS - 2 - k] + assert p < PRICE_MASK + packed_prices = p | packed_prices + + # pool is an ERC20 implementation + _salt: bytes32 = block.prevhash + _math_implementation: address = self.math_implementation + pool: address = create_from_blueprint( + pool_implementation, + _name, + _symbol, + _coins, + _math_implementation, + _weth, + _salt, + packed_precisions, + packed_A_gamma, + packed_fee_params, + packed_rebalancing_params, + packed_prices, + code_offset=3 + ) + + # populate pool data + length: uint256 = self.pool_count + self.pool_list[length] = pool + self.pool_count = length + 1 + self.pool_data[pool].decimals = decimals + self.pool_data[pool].coins = _coins + self.pool_data[pool].implementation = pool_implementation + + # add coins to market: + self._add_coins_to_market(_coins[0], _coins[1], pool) + self._add_coins_to_market(_coins[0], _coins[2], pool) + self._add_coins_to_market(_coins[1], _coins[2], pool) + + log TricryptoPoolDeployed( + pool, + _name, + _symbol, + _weth, + _coins, + _math_implementation, + _salt, + packed_precisions, + packed_A_gamma, + packed_fee_params, + packed_rebalancing_params, + packed_prices, + msg.sender, + ) + + return pool + + +@internal +def _add_coins_to_market(coin_a: address, coin_b: address, pool: address): + + key: uint256 = ( + convert(coin_a, uint256) ^ convert(coin_b, uint256) + ) + + length: uint256 = self.market_counts[key] + self.markets[key][length] = pool + self.market_counts[key] = length + 1 + + +# <--- Admin / Guarded Functionality ---> + + +@external +def set_fee_receiver(_fee_receiver: address): + """ + @notice Set fee receiver + @param _fee_receiver Address that fees are sent to + """ + assert msg.sender == self.admin, "dev: admin only" + + log UpdateFeeReceiver(self.fee_receiver, _fee_receiver) + self.fee_receiver = _fee_receiver + + +@external +def set_pool_implementation( + _pool_implementation: address, _implementation_index: uint256 +): + """ + @notice Set pool implementation + @dev Set to empty(address) to prevent deployment of new pools + @param _pool_implementation Address of the new pool implementation + @param _implementation_index Index of the pool implementation + """ + assert msg.sender == self.admin, "dev: admin only" + + log UpdatePoolImplementation( + _implementation_index, + self.pool_implementations[_implementation_index], + _pool_implementation + ) + + self.pool_implementations[_implementation_index] = _pool_implementation + + +@external +def set_views_implementation(_views_implementation: address): + """ + @notice Set views contract implementation + @param _views_implementation Address of the new views contract + """ + assert msg.sender == self.admin, "dev: admin only" + + log UpdateViewsImplementation(self.views_implementation, _views_implementation) + self.views_implementation = _views_implementation + + +@external +def set_math_implementation(_math_implementation: address): + """ + @notice Set math implementation + @param _math_implementation Address of the new math contract + """ + assert msg.sender == self.admin, "dev: admin only" + + log UpdateMathImplementation(self.math_implementation, _math_implementation) + self.math_implementation = _math_implementation + + +@external +def commit_transfer_ownership(_addr: address): + """ + @notice Transfer ownership of this contract to `addr` + @param _addr Address of the new owner + """ + assert msg.sender == self.admin, "dev: admin only" + + self.future_admin = _addr + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + @dev Only callable by the new owner + """ + assert msg.sender == self.future_admin, "dev: future admin only" + + log TransferOwnership(self.admin, msg.sender) + self.admin = msg.sender + + +# <--- Factory Getters ---> + + +@view +@external +def get_implementation_address(_pool: address) -> address: + """ + @notice Get the address of the implementation contract used for a factory pool + @param _pool Pool address + @return Implementation contract address + """ + return self.pool_data[_pool].implementation + + +@view +@external +def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: + """ + @notice Find an available pool for exchanging two coins + @param _from Address of coin to be sent + @param _to Address of coin to be received + @param i Index value. When multiple pools are available + this value is used to return the n'th address. + @return Pool address + """ + key: uint256 = convert(_from, uint256) ^ convert(_to, uint256) + return self.markets[key][i] + + +# <--- Pool Getters ---> + + +@view +@external +def get_coins(_pool: address) -> address[N_COINS]: + """ + @notice Get the coins within a pool + @param _pool Pool address + @return List of coin addresses + """ + return self.pool_data[_pool].coins + + +@view +@external +def get_decimals(_pool: address) -> uint256[N_COINS]: + """ + @notice Get decimal places for each coin within a pool + @param _pool Pool address + @return uint256 list of decimals + """ + return self.pool_data[_pool].decimals + + +@view +@external +def get_balances(_pool: address) -> uint256[N_COINS]: + """ + @notice Get balances for each coin within a pool + @dev For pools using lending, these are the wrapped coin balances + @param _pool Pool address + @return uint256 list of balances + """ + return [ + TricryptoPool(_pool).balances(0), + TricryptoPool(_pool).balances(1), + TricryptoPool(_pool).balances(2), + ] + + +@view +@external +def get_coin_indices( + _pool: address, + _from: address, + _to: address +) -> (uint256, uint256): + """ + @notice Convert coin addresses to indices for use with pool methods + @param _pool Pool address + @param _from Coin address to be used as `i` within a pool + @param _to Coin address to be used as `j` within a pool + @return uint256 `i`, uint256 `j` + """ + coins: address[N_COINS] = self.pool_data[_pool].coins + + for i in range(N_COINS): + for j in range(N_COINS): + if i == j: + continue + + if coins[i] == _from and coins[j] == _to: + return i, j + + raise "Coins not found" + + +@view +@external +def get_market_counts(coin_a: address, coin_b: address) -> uint256: + """ + @notice Gets the number of markets with the specified coins. + @return Number of pools with the input coins + """ + + key: uint256 = ( + convert(coin_a, uint256) ^ convert(coin_b, uint256) + ) + + return self.market_counts[key] diff --git a/contracts/amms/tricryptong/CurveTricryptoFactory.vy b/contracts/amms/tricryptong/CurveTricryptoFactory.vy new file mode 100644 index 0000000..664744b --- /dev/null +++ b/contracts/amms/tricryptong/CurveTricryptoFactory.vy @@ -0,0 +1,502 @@ +# @version 0.3.10 + +""" +@title CurveTricryptoFactory +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice Permissionless 3-coin cryptoswap pool deployer and registry +""" + +interface TricryptoPool: + def balances(i: uint256) -> uint256: view + +interface ERC20: + def decimals() -> uint256: view + + +event TricryptoPoolDeployed: + pool: address + name: String[64] + symbol: String[32] + weth: address + coins: address[N_COINS] + math: address + salt: bytes32 + packed_precisions: uint256 + packed_A_gamma: uint256 + packed_fee_params: uint256 + packed_rebalancing_params: uint256 + packed_prices: uint256 + deployer: address + + +event 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] + + +N_COINS: constant(uint256) = 3 +A_MULTIPLIER: constant(uint256) = 10000 + +# Limits +MAX_FEE: constant(uint256) = 10 * 10 ** 9 + +MIN_GAMMA: constant(uint256) = 10 ** 10 +MAX_GAMMA: constant(uint256) = 5 * 10**16 + +MIN_A: constant(uint256) = N_COINS ** N_COINS * A_MULTIPLIER / 100 +MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS + +PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1) +PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1 + +admin: public(address) +future_admin: public(address) + +# fee receiver for all pools: +fee_receiver: public(address) + +pool_implementations: public(HashMap[uint256, address]) +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, address[4294967296]] +market_counts: HashMap[uint256, uint256] + +pool_count: public(uint256) # actual length of pool_list +pool_data: HashMap[address, PoolArray] +pool_list: public(address[4294967296]) # master list of pools + + +@external +def __init__(_fee_receiver: address, _admin: address): + + self.fee_receiver = _fee_receiver + self.admin = _admin + + log UpdateFeeReceiver(empty(address), _fee_receiver) + log TransferOwnership(empty(address), _admin) + + +@internal +@view +def _pack(x: uint256[3]) -> uint256: + """ + @notice Packs 3 integers with values <= 10**18 into a uint256 + @param x The uint256[3] to pack + @return The packed uint256 + """ + return (x[0] << 128) | (x[1] << 64) | x[2] + + + +# <--- Pool Deployers ---> + +@external +def deploy_pool( + _name: String[64], + _symbol: String[32], + _coins: address[N_COINS], + _weth: address, + implementation_id: uint256, + A: uint256, + gamma: uint256, + mid_fee: uint256, + out_fee: uint256, + fee_gamma: uint256, + allowed_extra_profit: uint256, + adjustment_step: uint256, + ma_exp_time: uint256, + initial_prices: uint256[N_COINS-1], +) -> address: + """ + @notice Deploy a new pool + @param _name Name of the new plain pool + @param _symbol Symbol for the new plain pool - will be concatenated with factory symbol + + @return Address of the deployed pool + """ + pool_implementation: address = self.pool_implementations[implementation_id] + assert pool_implementation != empty(address), "Pool implementation not set" + + # Validate parameters + assert A > MIN_A-1 + assert A < MAX_A+1 + + assert gamma > MIN_GAMMA-1 + assert gamma < MAX_GAMMA+1 + + assert mid_fee < MAX_FEE-1 # mid_fee can be zero + assert out_fee >= mid_fee + assert out_fee < MAX_FEE-1 + assert fee_gamma < 10**18+1 + assert fee_gamma > 0 + + assert allowed_extra_profit < 10**18+1 + + assert adjustment_step < 10**18+1 + assert adjustment_step > 0 + + assert ma_exp_time < 872542 # 7 * 24 * 60 * 60 / ln(2) + assert ma_exp_time > 86 # 60 / ln(2) + + assert min(initial_prices[0], initial_prices[1]) > 10**6 + assert max(initial_prices[0], initial_prices[1]) < 10**30 + + assert _coins[0] != _coins[1] and _coins[1] != _coins[2] and _coins[0] != _coins[2], "Duplicate coins" + + decimals: uint256[N_COINS] = empty(uint256[N_COINS]) + precisions: uint256[N_COINS] = empty(uint256[N_COINS]) + for i in range(N_COINS): + d: uint256 = ERC20(_coins[i]).decimals() + assert d < 19, "Max 18 decimals for coins" + decimals[i] = d + precisions[i] = 10** (18 - d) + + # pack precisions + packed_precisions: uint256 = self._pack(precisions) + + # pack fees + packed_fee_params: uint256 = self._pack( + [mid_fee, out_fee, fee_gamma] + ) + + # pack liquidity rebalancing params + packed_rebalancing_params: uint256 = self._pack( + [allowed_extra_profit, adjustment_step, ma_exp_time] + ) + + # pack A_gamma + packed_A_gamma: uint256 = A << 128 + packed_A_gamma = packed_A_gamma | gamma + + # pack initial prices + packed_prices: uint256 = 0 + for k in range(N_COINS - 1): + packed_prices = packed_prices << PRICE_SIZE + p: uint256 = initial_prices[N_COINS - 2 - k] + assert p < PRICE_MASK + packed_prices = p | packed_prices + + # pool is an ERC20 implementation + _salt: bytes32 = block.prevhash + _math_implementation: address = self.math_implementation + pool: address = create_from_blueprint( + pool_implementation, + _name, + _symbol, + _coins, + _math_implementation, + _weth, + _salt, + packed_precisions, + packed_A_gamma, + packed_fee_params, + packed_rebalancing_params, + packed_prices, + code_offset=3 + ) + + # populate pool data + length: uint256 = self.pool_count + self.pool_list[length] = pool + self.pool_count = length + 1 + self.pool_data[pool].decimals = decimals + self.pool_data[pool].coins = _coins + + # add coins to market: + self._add_coins_to_market(_coins[0], _coins[1], pool) + self._add_coins_to_market(_coins[0], _coins[2], pool) + self._add_coins_to_market(_coins[1], _coins[2], pool) + + log TricryptoPoolDeployed( + pool, + _name, + _symbol, + _weth, + _coins, + _math_implementation, + _salt, + packed_precisions, + packed_A_gamma, + packed_fee_params, + packed_rebalancing_params, + packed_prices, + msg.sender, + ) + + return pool + + +@internal +def _add_coins_to_market(coin_a: address, coin_b: address, pool: address): + + key: uint256 = ( + convert(coin_a, uint256) ^ convert(coin_b, uint256) + ) + + length: uint256 = self.market_counts[key] + self.markets[key][length] = pool + self.market_counts[key] = length + 1 + + +@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 get_coins(_pool: address) -> address[N_COINS]: + """ + @notice Get the coins within a pool + @param _pool Pool address + @return List of coin addresses + """ + return self.pool_data[_pool].coins + + +@view +@external +def get_decimals(_pool: address) -> uint256[N_COINS]: + """ + @notice Get decimal places for each coin within a pool + @param _pool Pool address + @return uint256 list of decimals + """ + return self.pool_data[_pool].decimals + + +@view +@external +def get_balances(_pool: address) -> uint256[N_COINS]: + """ + @notice Get balances for each coin within a pool + @dev For pools using lending, these are the wrapped coin balances + @param _pool Pool address + @return uint256 list of balances + """ + return [ + TricryptoPool(_pool).balances(0), + TricryptoPool(_pool).balances(1), + TricryptoPool(_pool).balances(2), + ] + + +@view +@external +def get_coin_indices( + _pool: address, + _from: address, + _to: address +) -> (uint256, uint256): + """ + @notice Convert coin addresses to indices for use with pool methods + @param _pool Pool address + @param _from Coin address to be used as `i` within a pool + @param _to Coin address to be used as `j` within a pool + @return uint256 `i`, uint256 `j` + """ + coins: address[N_COINS] = self.pool_data[_pool].coins + + for i in range(N_COINS): + for j in range(N_COINS): + if i == j: + continue + + if coins[i] == _from and coins[j] == _to: + return i, j + + raise "Coins not found" + + +@view +@external +def get_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 self.market_counts[key] diff --git a/contracts/amms/tricryptong/CurveTricryptoOptimized.vy b/contracts/amms/tricryptong/CurveTricryptoOptimized.vy new file mode 100644 index 0000000..8d050cd --- /dev/null +++ b/contracts/amms/tricryptong/CurveTricryptoOptimized.vy @@ -0,0 +1,2081 @@ +# pragma version 0.3.10 +# pragma optimize gas +# pragma evm-version paris +""" +@title CurveTricryptoOptimized +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2023 - all rights reserved +@notice A Curve AMM pool for 3 unpegged assets (e.g. WETH, BTC, USD). +@dev All prices in the AMM are with respect to the first token in the pool. +""" + +from vyper.interfaces import ERC20 +implements: ERC20 # <--------------------- AMM contract is also the LP token. + +# --------------------------------- Interfaces ------------------------------- + +interface Math: + def geometric_mean(_x: uint256[N_COINS]) -> uint256: view + def wad_exp(_power: int256) -> uint256: view + def cbrt(x: uint256) -> uint256: view + def reduction_coefficient( + x: uint256[N_COINS], fee_gamma: uint256 + ) -> uint256: view + def newton_D( + ANN: uint256, + gamma: uint256, + x_unsorted: uint256[N_COINS], + K0_prev: uint256 + ) -> uint256: view + def get_y( + ANN: uint256, + gamma: uint256, + x: uint256[N_COINS], + D: uint256, + i: uint256, + ) -> uint256[2]: view + def get_p( + _xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[2], + ) -> uint256[N_COINS-1]: view + +interface Factory: + def admin() -> address: view + def fee_receiver() -> address: view + def views_implementation() -> address: view + +interface Views: + def calc_token_amount( + amounts: uint256[N_COINS], deposit: bool, swap: address + ) -> uint256: view + def get_dy( + i: uint256, j: uint256, dx: uint256, swap: address + ) -> uint256: view + def get_dx( + i: uint256, j: uint256, dy: uint256, swap: address + ) -> uint256: view + + +# ------------------------------- Events ------------------------------------- + +event Transfer: + sender: indexed(address) + receiver: indexed(address) + value: uint256 + +event Approval: + owner: indexed(address) + spender: indexed(address) + value: uint256 + +event TokenExchange: + buyer: indexed(address) + sold_id: uint256 + tokens_sold: uint256 + bought_id: uint256 + tokens_bought: uint256 + fee: uint256 + packed_price_scale: uint256 + +event AddLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fee: uint256 + token_supply: uint256 + packed_price_scale: uint256 + +event RemoveLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + token_supply: uint256 + +event RemoveLiquidityOne: + provider: indexed(address) + token_amount: uint256 + coin_index: uint256 + coin_amount: uint256 + approx_fee: uint256 + packed_price_scale: uint256 + +event NewParameters: + mid_fee: uint256 + out_fee: uint256 + fee_gamma: uint256 + allowed_extra_profit: uint256 + adjustment_step: uint256 + ma_time: uint256 + xcp_ma_time: uint256 + +event RampAgamma: + initial_A: uint256 + future_A: uint256 + initial_gamma: uint256 + future_gamma: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + current_A: uint256 + current_gamma: uint256 + time: uint256 + +event ClaimAdminFee: + admin: indexed(address) + tokens: uint256[N_COINS] + + +# ----------------------- Storage/State Variables ---------------------------- + +N_COINS: constant(uint256) = 3 +PRECISION: constant(uint256) = 10**18 # <------- The precision to convert to. +PRECISIONS: immutable(uint256[N_COINS]) + +MATH: public(immutable(Math)) +coins: public(immutable(address[N_COINS])) +factory: public(immutable(Factory)) + +price_scale_packed: uint256 # <------------------------ Internal price scale. +price_oracle_packed: uint256 # <------- Price target given by moving average. +cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. + +last_prices_packed: uint256 +last_timestamp: public(uint256) # idx 0 is for prices, idx 1 is for xcp. +last_xcp: public(uint256) +xcp_ma_time: public(uint256) + +initial_A_gamma: public(uint256) +initial_A_gamma_time: public(uint256) + +future_A_gamma: public(uint256) +future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. +# This value is 0 (default) when pool is first deployed, and only gets +# populated by block.timestamp + future_time in `ramp_A_gamma` when the +# ramping process is initiated. After ramping is finished +# (i.e. self.future_A_gamma_time < block.timestamp), the variable is left +# and not set to 0. + +balances: public(uint256[N_COINS]) +D: public(uint256) +xcp_profit: public(uint256) +xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees. + +virtual_price: public(uint256) # <------ Cached (fast to read) virtual price. +# The cached `virtual_price` is also used internally. + +# Params that affect how price_scale get adjusted : +packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing +# parameters allowed_extra_profit, adjustment_step, and ma_time. + +# Fee params that determine dynamic fees: +packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma. + +ADMIN_FEE: public(constant(uint256)) = 5 * 10**9 # <----- 50% of earned fees. +MIN_FEE: constant(uint256) = 5 * 10**5 # <-------------------------- 0.5 BPS. +MAX_FEE: constant(uint256) = 10 * 10**9 +NOISE_FEE: constant(uint256) = 10**5 # <---------------------------- 0.1 BPS. + +# ----------------------- Admin params --------------------------------------- + +last_admin_fee_claim_timestamp: uint256 +admin_lp_virtual_balance: uint256 + +MIN_RAMP_TIME: constant(uint256) = 86400 +MIN_ADMIN_FEE_CLAIM_INTERVAL: constant(uint256) = 86400 + +A_MULTIPLIER: constant(uint256) = 10000 +MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 100 +MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 +MAX_A_CHANGE: constant(uint256) = 10 +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA: constant(uint256) = 5 * 10**16 + +PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1) +PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1 + +# ----------------------- ERC20 Specific vars -------------------------------- + +name: public(immutable(String[64])) +symbol: public(immutable(String[32])) +decimals: public(constant(uint8)) = 18 +version: public(constant(String[8])) = "v2.0.0" + +balanceOf: public(HashMap[address, uint256]) +allowance: public(HashMap[address, HashMap[address, uint256]]) +totalSupply: public(uint256) +nonces: public(HashMap[address, uint256]) + +EIP712_TYPEHASH: constant(bytes32) = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" +) +EIP2612_TYPEHASH: constant(bytes32) = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +) +VERSION_HASH: constant(bytes32) = keccak256(version) +NAME_HASH: immutable(bytes32) +CACHED_CHAIN_ID: immutable(uint256) +salt: public(immutable(bytes32)) +CACHED_DOMAIN_SEPARATOR: immutable(bytes32) + + +# ----------------------- Contract ------------------------------------------- + +@external +def __init__( + _name: String[64], + _symbol: String[32], + _coins: address[N_COINS], + _math: address, + _weth: address, # unused but factory has it. + _salt: bytes32, + __packed_precisions: uint256, + packed_A_gamma: uint256, + packed_fee_params: uint256, + packed_rebalancing_params: uint256, + packed_prices: uint256, +): + MATH = Math(_math) + factory = Factory(msg.sender) + name = _name + symbol = _symbol + coins = _coins + + PRECISIONS = self._unpack_3(__packed_precisions) # <------- Precisions of + # coins are calculated as 10**(18 - coin.decimals()). + + self.initial_A_gamma = packed_A_gamma # <------------------- A and gamma. + self.future_A_gamma = packed_A_gamma + + self.packed_rebalancing_params = packed_rebalancing_params # <-- Contains + # rebalancing params: allowed_extra_profit, adjustment_step, + # and ma_exp_time. + + self.packed_fee_params = packed_fee_params # <-------------- Contains Fee + # params: mid_fee, out_fee and fee_gamma. + + self.price_scale_packed = packed_prices + self.price_oracle_packed = packed_prices + self.last_prices_packed = packed_prices + self.last_timestamp = self._pack_2(block.timestamp, block.timestamp) + self.xcp_profit_a = 10**18 + self.xcp_ma_time = 62324 # <--------- 12 hours default on contract start. + + # Cache DOMAIN_SEPARATOR. If chain.id is not CACHED_CHAIN_ID, then + # DOMAIN_SEPARATOR will be re-calculated each time `permit` is called. + # Otherwise, it will always use CACHED_DOMAIN_SEPARATOR. + # see: `_domain_separator()` for its implementation. + NAME_HASH = keccak256(name) + salt = _salt + CACHED_CHAIN_ID = chain.id + CACHED_DOMAIN_SEPARATOR = keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + + log Transfer(empty(address), self, 0) # <------- Fire empty transfer from + # 0x0 to self for indexers to catch. + + +# ------------------- Token transfers in and out of the AMM ------------------ + + +@internal +def _transfer_in( + _coin_idx: uint256, + _dx: uint256, + sender: address, + expect_optimistic_transfer: bool, +) -> uint256: + """ + @notice Transfers `_coin` from `sender` to `self` and calls `callback_sig` + if it is not empty. + @params _coin_idx uint256 Index of the coin to transfer in. + @params dx amount of `_coin` to transfer into the pool. + @params sender address to transfer `_coin` from. + @params expect_optimistic_transfer bool True if pool expects user to transfer. + This is only enabled for exchange_received. + @return The amount of tokens received. + """ + coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) + + if expect_optimistic_transfer: # Only enabled in exchange_received: + # it expects the caller of exchange_received to have sent tokens to + # the pool before calling this method. + + # If someone donates extra tokens to the contract: do not acknowledge. + # We only want to know if there are dx amount of tokens. Anything extra, + # we ignore. This is why we need to check if received_amounts (which + # accounts for coin balances of the contract) is atleast dx. + # If we checked for received_amounts == dx, an extra transfer without a + # call to exchange_received will break the method. + dx: uint256 = coin_balance - self.balances[_coin_idx] + assert dx >= _dx # dev: user didn't give us coins + + # Adjust balances + self.balances[_coin_idx] += dx + + return dx + + # ----------------------------------------------- ERC20 transferFrom flow. + + # EXTERNAL CALL + assert ERC20(coins[_coin_idx]).transferFrom( + sender, + self, + _dx, + default_return_value=True + ) + + dx: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance + self.balances[_coin_idx] += dx + return dx + + +@internal +def _transfer_out(_coin_idx: uint256, _amount: uint256, receiver: address): + """ + @notice Transfer a single token from the pool to receiver. + @dev This function is called by `remove_liquidity` and + `remove_liquidity_one`, `_claim_admin_fees` and `_exchange` methods. + @params _coin_idx uint256 Index of the token to transfer out + @params _amount Amount of token to transfer out + @params receiver Address to send the tokens to + """ + + # Adjust balances before handling transfers: + self.balances[_coin_idx] -= _amount + + # EXTERNAL CALL + assert ERC20(coins[_coin_idx]).transfer( + receiver, + _amount, + default_return_value=True + ) + + +# -------------------------- AMM Main Functions ------------------------------ + + +@external +@nonreentrant("lock") +def exchange( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + receiver: address = msg.sender +) -> uint256: + """ + @notice Exchange using wrapped native token by default + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param receiver Address to send the output coin to. Default is msg.sender + @return uint256 Amount of tokens at index j received by the `receiver + """ + # _transfer_in updates self.balances here: + dx_received: uint256 = self._transfer_in( + i, + dx, + msg.sender, + False + ) + + # No ERC20 token transfers occur here: + out: uint256[3] = self._exchange( + i, + j, + dx_received, + min_dy, + ) + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(j, out[0], receiver) + + # log: + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) + + return out[0] + + +@external +@nonreentrant('lock') +def exchange_received( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + receiver: address = msg.sender, +) -> uint256: + """ + @notice Exchange: but user must transfer dx amount of coin[i] tokens to pool first. + Pool will not call transferFrom and will only check if a surplus of + coins[i] is greater than or equal to `dx`. + @dev Use-case is to reduce the number of redundant ERC20 token + transfers in zaps. Primarily for dex-aggregators/arbitrageurs/searchers. + Note for users: please transfer + exchange_received in 1 tx. + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param receiver Address to send the output coin to + @return uint256 Amount of tokens at index j received by the `receiver` + """ + # _transfer_in updates self.balances here: + dx_received: uint256 = self._transfer_in( + i, + dx, + msg.sender, + True # <---- expect_optimistic_transfer is set to True here. + ) + + # No ERC20 token transfers occur here: + out: uint256[3] = self._exchange( + i, + j, + dx_received, + min_dy, + ) + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(j, out[0], receiver) + + # log: + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) + + return out[0] + + +@external +@nonreentrant("lock") +def add_liquidity( + amounts: uint256[N_COINS], + min_mint_amount: uint256, + receiver: address = msg.sender +) -> uint256: + """ + @notice Adds liquidity into the pool. + @param amounts Amounts of each coin to add. + @param min_mint_amount Minimum amount of LP to mint. + @param receiver Address to send the LP tokens to. Default is msg.sender + @return uint256 Amount of LP tokens received by the `receiver + """ + + A_gamma: uint256[2] = self._A_gamma() + xp: uint256[N_COINS] = self.balances + amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) + d_token: uint256 = 0 + d_token_fee: uint256 = 0 + old_D: uint256 = 0 + + assert amounts[0] + amounts[1] + amounts[2] > 0 # dev: no coins to add + + # --------------------- Get prices, balances ----------------------------- + + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS-1] = self._unpack_prices(packed_price_scale) + + # -------------------------------------- Update balances and calculate xp. + xp_old: uint256[N_COINS] = xp + amounts_received: uint256[N_COINS] = empty(uint256[N_COINS]) + + ########################## TRANSFER IN <------- + + for i in range(N_COINS): + if amounts[i] > 0: + # Updates self.balances here: + amounts_received[i] = self._transfer_in( + i, + amounts[i], + msg.sender, + False, # <--------------------- Disable optimistic transfers. + ) + xp[i] = xp[i] + amounts_received[i] + + xp[0] *= PRECISIONS[0] + xp_old[0] *= PRECISIONS[0] + for i in range(N_COINS): + + if i >= 1: + xp[i] = unsafe_div(xp[i] * price_scale[i-1] * PRECISIONS[i], PRECISION) + xp_old[i] = unsafe_div( + xp_old[i] * unsafe_mul(price_scale[i-1], PRECISIONS[i]), + PRECISION + ) + + if amounts_received[i] > 0: + amountsp[i] = xp[i] - xp_old[i] + + # -------------------- Calculate LP tokens to mint ----------------------- + + if self.future_A_gamma_time > block.timestamp: # <--- A_gamma is ramping. + + # ----- Recalculate the invariant if A or gamma are undergoing a ramp. + old_D = MATH.newton_D(A_gamma[0], A_gamma[1], xp_old, 0) + + else: + + old_D = self.D + + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + + token_supply: uint256 = self.totalSupply + if old_D > 0: + d_token = token_supply * D / old_D - token_supply + else: + d_token = self.get_xcp(D, packed_price_scale) # <----- Making initial + # virtual price equal to 1. + + assert d_token > 0 # dev: nothing minted + + if old_D > 0: + + d_token_fee = ( + self._calc_token_fee(amountsp, xp) * d_token / 10**10 + 1 + ) + + d_token -= d_token_fee + token_supply += d_token + self.mint(receiver, d_token) + self.admin_lp_virtual_balance += unsafe_div(ADMIN_FEE * d_token_fee, 10**10) + + packed_price_scale = self.tweak_price(A_gamma, xp, D, 0) + + else: + + # (re)instatiating an empty pool: + + self.D = D + self.virtual_price = 10**18 + self.xcp_profit = 10**18 + self.xcp_profit_a = 10**18 + + # Initialise xcp oracle here: + self.cached_xcp_oracle = d_token # <--- virtual_price * totalSupply / 10**18 + + self.mint(receiver, d_token) + + assert d_token >= min_mint_amount, "Slippage" + + # ---------------------------------------------- Log and claim admin fees. + + log AddLiquidity( + receiver, amounts_received, d_token_fee, token_supply, packed_price_scale + ) + + return d_token + + +@external +@nonreentrant("lock") +def remove_liquidity( + _amount: uint256, + min_amounts: uint256[N_COINS], + receiver: address = msg.sender, +) -> uint256[N_COINS]: + """ + @notice This withdrawal method is very safe, does no complex math since + tokens are withdrawn in balanced proportions. No fees are charged. + @param _amount Amount of LP tokens to burn + @param min_amounts Minimum amounts of tokens to withdraw + @param receiver Address to send the withdrawn tokens to + @return uint256[3] Amount of pool tokens received by the `receiver` + """ + amount: uint256 = _amount + balances: uint256[N_COINS] = self.balances + withdraw_amounts: uint256[N_COINS] = empty(uint256[N_COINS]) + + # -------------------------------------------------------- Burn LP tokens. + + total_supply: uint256 = self.totalSupply # <------ Get totalSupply before + self.burnFrom(msg.sender, _amount) # ---- reducing it with self.burnFrom. + + # There are two cases for withdrawing tokens from the pool. + # Case 1. Withdrawal does not empty the pool. + # In this situation, D is adjusted proportional to the amount of + # LP tokens burnt. ERC20 tokens transferred is proportional + # to : (AMM balance * LP tokens in) / LP token total supply + # Case 2. Withdrawal empties the pool. + # In this situation, all tokens are withdrawn and the invariant + # is reset. + + if amount == total_supply: # <----------------------------------- Case 2. + + for i in range(N_COINS): + + withdraw_amounts[i] = balances[i] + + else: # <-------------------------------------------------------- Case 1. + + amount -= 1 # <---- To prevent rounding errors, favor LPs a tiny bit. + + for i in range(N_COINS): + + withdraw_amounts[i] = balances[i] * amount / total_supply + assert withdraw_amounts[i] >= min_amounts[i] + + D: uint256 = self.D + self.D = D - unsafe_div(D * amount, total_supply) # <----------- Reduce D + # proportional to the amount of tokens leaving. Since withdrawals are + # balanced, this is a simple subtraction. If amount == total_supply, + # D will be 0. + + # ---------------------------------- Transfers --------------------------- + + for i in range(N_COINS): + # _transfer_out updates self.balances here. Update to state occurs + # before external calls: + self._transfer_out(i, withdraw_amounts[i], receiver) + + log RemoveLiquidity(msg.sender, withdraw_amounts, total_supply - _amount) + + # --------------------------- Upkeep xcp oracle -------------------------- + + # Update xcp since liquidity was removed: + xp: uint256[N_COINS] = self.xp(self.balances, self.price_scale_packed) + last_xcp: uint256 = MATH.geometric_mean(xp) # <----------- Cache it for now. + + last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) + if last_timestamp[1] < block.timestamp: + + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + alpha: uint256 = self._alpha(last_timestamp[1], self.xcp_ma_time) + + self.cached_xcp_oracle = unsafe_div( + last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, + 10**18 + ) + last_timestamp[1] = block.timestamp + + # Pack and store timestamps: + self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) + + # Store last xcp + self.last_xcp = last_xcp + + return withdraw_amounts + + +@external +@nonreentrant("lock") +def remove_liquidity_one_coin( + token_amount: uint256, + i: uint256, + min_amount: uint256, + receiver: address = msg.sender +) -> uint256: + """ + @notice Withdraw liquidity in a single token. + Involves fees (lower than swap fees). + @dev This operation also involves an admin fee claim. + @param token_amount Amount of LP tokens to burn + @param i Index of the token to withdraw + @param min_amount Minimum amount of token to withdraw. + @param receiver Address to send the withdrawn tokens to + @return Amount of tokens at index i received by the `receiver` + """ + + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. + + A_gamma: uint256[2] = self._A_gamma() + + dy: uint256 = 0 + D: uint256 = 0 + p: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + approx_fee: uint256 = 0 + + # ------------------------------------------------------------------------ + + dy, D, xp, approx_fee = self._calc_withdraw_one_coin( + A_gamma, + token_amount, + i, + (self.future_A_gamma_time > block.timestamp), # <------- During ramps + ) # we need to update D. + + assert dy >= min_amount, "Slippage" + + # ---------------------------- State Updates ----------------------------- + + # Burn user's tokens: + self.burnFrom(msg.sender, token_amount) + + packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0) + # Safe to use D from _calc_withdraw_one_coin here ---^ + + # ------------------------- Transfers ------------------------------------ + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(i, dy, receiver) + + log RemoveLiquidityOne( + msg.sender, token_amount, i, dy, approx_fee, packed_price_scale + ) + + return dy + + +# -------------------------- Packing functions ------------------------------- + + +@internal +@pure +def _pack_3(x: uint256[3]) -> uint256: + """ + @notice Packs 3 integers with values <= 10**18 into a uint256 + @param x The uint256[3] to pack + @return uint256 Integer with packed values + """ + return (x[0] << 128) | (x[1] << 64) | x[2] + + +@internal +@pure +def _unpack_3(_packed: uint256) -> uint256[3]: + """ + @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) + @param val The uint256 to unpack + @return uint256[3] A list of length 3 with unpacked integers + """ + return [ + (_packed >> 128) & 18446744073709551615, + (_packed >> 64) & 18446744073709551615, + _packed & 18446744073709551615, + ] + + + +@pure +@internal +def _pack_2(p1: uint256, p2: uint256) -> uint256: + return p1 | (p2 << 128) + + +@pure +@internal +def _unpack_2(packed: uint256) -> uint256[2]: + return [packed & (2**128 - 1), packed >> 128] + + +@internal +@pure +def _pack_prices(prices_to_pack: uint256[N_COINS-1]) -> uint256: + """ + @notice Packs N_COINS-1 prices into a uint256. + @param prices_to_pack The prices to pack + @return uint256 An integer that packs prices + """ + packed_prices: uint256 = 0 + p: uint256 = 0 + for k in range(N_COINS - 1): + packed_prices = packed_prices << PRICE_SIZE + p = prices_to_pack[N_COINS - 2 - k] + assert p < PRICE_MASK + packed_prices = p | packed_prices + return packed_prices + + +@internal +@pure +def _unpack_prices(_packed_prices: uint256) -> uint256[2]: + """ + @notice Unpacks N_COINS-1 prices from a uint256. + @param _packed_prices The packed prices + @return uint256[2] Unpacked prices + """ + unpacked_prices: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + packed_prices: uint256 = _packed_prices + for k in range(N_COINS - 1): + unpacked_prices[k] = packed_prices & PRICE_MASK + packed_prices = packed_prices >> PRICE_SIZE + + return unpacked_prices + + +# ---------------------- AMM Internal Functions ------------------------------- + + +@internal +def _exchange( + i: uint256, + j: uint256, + dx_received: uint256, + min_dy: uint256, +) -> uint256[3]: + + assert i != j # dev: coin index out of range + assert dx_received > 0 # dev: do not exchange 0 coins + + A_gamma: uint256[2] = self._A_gamma() + xp: uint256[N_COINS] = self.balances # <------- Has dx added to balances. + dy: uint256 = 0 + + y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert. + x0: uint256 = xp[i] - dx_received # old xp[i] + + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS - 1] = self._unpack_prices( + packed_price_scale + ) + + xp[0] *= PRECISIONS[0] + for k in range(1, N_COINS): + xp[k] = unsafe_div( + xp[k] * price_scale[k - 1] * PRECISIONS[k], + PRECISION + ) # <-------- Safu to do unsafe_div here since PRECISION is not zero. + + prec_i: uint256 = PRECISIONS[i] + + # ----------- Update invariant if A, gamma are undergoing ramps --------- + + t: uint256 = self.future_A_gamma_time + if t > block.timestamp: + + x0 *= prec_i + + if i > 0: + x0 = unsafe_div(x0 * price_scale[i - 1], PRECISION) + + x1: uint256 = xp[i] # <------------------ Back up old value in xp ... + xp[i] = x0 # | + self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # | + xp[i] = x1 # <-------------------------------------- ... and restore. + + # ----------------------- Calculate dy and fees -------------------------- + + D: uint256 = self.D + y_out: uint256[2] = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, j) + dy = xp[j] - y_out[0] + xp[j] -= dy + dy -= 1 + + if j > 0: + dy = dy * PRECISION / price_scale[j - 1] + dy /= PRECISIONS[j] + + fee: uint256 = unsafe_div(self._fee(xp) * dy, 10**10) + dy -= fee # <--------------------- Subtract fee from the outgoing amount. + assert dy >= min_dy, "Slippage" + + y -= dy + + y *= PRECISIONS[j] + if j > 0: + y = unsafe_div(y * price_scale[j - 1], PRECISION) + xp[j] = y # <------------------------------------------------- Update xp. + + # ------ Tweak price_scale with good initial guess for newton_D ---------- + + packed_price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1]) + + return [dy, fee, packed_price_scale] + + +@internal +def tweak_price( + A_gamma: uint256[2], + _xp: uint256[N_COINS], + new_D: uint256, + K0_prev: uint256 = 0, +) -> uint256: + """ + @notice Updates price_oracle, last_price and conditionally adjusts + price_scale. This is called whenever there is an unbalanced + liquidity operation: _exchange, add_liquidity, or + remove_liquidity_one_coin. + @dev Contains main liquidity rebalancing logic, by tweaking `price_scale`. + @param A_gamma Array of A and gamma parameters. + @param _xp Array of current balances. + @param new_D New D value. + @param K0_prev Initial guess for `newton_D`. + """ + + # ---------------------------- Read storage ------------------------------ + + price_oracle: uint256[N_COINS - 1] = self._unpack_prices(self.price_oracle_packed) + last_prices: uint256[N_COINS - 1] = self._unpack_prices(self.last_prices_packed) + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS - 1] = self._unpack_prices(packed_price_scale) + rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params) + # Contains: allowed_extra_profit, adjustment_step, ma_time. -----^ + + total_supply: uint256 = self.totalSupply + old_xcp_profit: uint256 = self.xcp_profit + old_virtual_price: uint256 = self.virtual_price + + # ----------------------- Update Oracles if needed ----------------------- + + last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) + alpha: uint256 = 0 + if last_timestamp[0] < block.timestamp: # 0th index is for price_oracle. + + # The moving average price oracle is calculated using the last_price + # of the trade at the previous block, and the price oracle logged + # before that trade. This can happen only once per block. + + # ------------------ Calculate moving average params ----------------- + + alpha = self._alpha(last_timestamp[0], rebalancing_params[2]) + for k in range(N_COINS - 1): + + # ----------------- We cap state price that goes into the EMA with + # 2 x price_scale. + price_oracle[k] = unsafe_div( + min(last_prices[k], 2 * price_scale[k]) * (10**18 - alpha) + + price_oracle[k] * alpha, # ^-------- Cap spot price into EMA. + 10**18 + ) + + self.price_oracle_packed = self._pack_prices(price_oracle) + last_timestamp[0] = block.timestamp + + # ----------------------------------------------------- Update xcp oracle. + + if last_timestamp[1] < block.timestamp: + + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + alpha = self._alpha(last_timestamp[1], self.xcp_ma_time) + self.cached_xcp_oracle = unsafe_div( + self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, + 10**18 + ) + + # Pack and store timestamps: + last_timestamp[1] = block.timestamp + + self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) + + # `price_oracle` is used further on to calculate its vector distance from + # price_scale. This distance is used to calculate the amount of adjustment + # to be done to the price_scale. + # ------------------------------------------------------------------------ + + # ------------------ If new_D is set to 0, calculate it ------------------ + + D_unadjusted: uint256 = new_D + if new_D == 0: # <--------------------------- _exchange sets new_D to 0. + D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev) + + # ----------------------- Calculate last_prices -------------------------- + + last_prices = MATH.get_p(_xp, D_unadjusted, A_gamma) + for k in range(N_COINS - 1): + last_prices[k] = unsafe_div(last_prices[k] * price_scale[k], 10**18) + self.last_prices_packed = self._pack_prices(last_prices) + + # ---------- Update profit numbers without price adjustment first -------- + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + xp[0] = unsafe_div(D_unadjusted, N_COINS) + for k in range(N_COINS - 1): + xp[k + 1] = D_unadjusted * 10**18 / (N_COINS * price_scale[k]) + + # ------------------------- Update xcp_profit ---------------------------- + + xcp_profit: uint256 = 10**18 + virtual_price: uint256 = 10**18 + + if old_virtual_price > 0: + + xcp: uint256 = MATH.geometric_mean(xp) + virtual_price = 10**18 * xcp / total_supply + + xcp_profit = unsafe_div( + old_xcp_profit * virtual_price, + old_virtual_price + ) # <---------------- Safu to do unsafe_div as old_virtual_price > 0. + + # If A and gamma are not undergoing ramps (t < block.timestamp), + # ensure new virtual_price is not less than old virtual_price, + # else the pool suffers a loss. + if self.future_A_gamma_time < block.timestamp: + assert virtual_price > old_virtual_price, "Loss" + + # -------------------------- Cache last_xcp -------------------------- + + self.last_xcp = xcp # geometric_mean(D * price_scale) + + self.xcp_profit = xcp_profit + + # ------------ Rebalance liquidity if there's enough profits to adjust it: + if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]: + # allowed_extra_profit --------^ + + # ------------------- Get adjustment step ---------------------------- + + # Calculate the vector distance between price_scale and + # price_oracle. + norm: uint256 = 0 + ratio: uint256 = 0 + for k in range(N_COINS - 1): + + ratio = unsafe_div(price_oracle[k] * 10**18, price_scale[k]) + # unsafe_div because we did safediv before ----^ + + if ratio > 10**18: + ratio = unsafe_sub(ratio, 10**18) + else: + ratio = unsafe_sub(10**18, ratio) + norm = unsafe_add(norm, ratio**2) + + norm = isqrt(norm) # <-------------------- isqrt is not in base 1e18. + adjustment_step: uint256 = max( + rebalancing_params[1], unsafe_div(norm, 5) + ) # ^------------------------------------- adjustment_step. + + if norm > adjustment_step: # <---------- We only adjust prices if the + # vector distance between price_oracle and price_scale is + # large enough. This check ensures that no rebalancing + # occurs if the distance is low i.e. the pool prices are + # pegged to the oracle prices. + + # ------------------------------------- Calculate new price scale. + + p_new: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1]) + for k in range(N_COINS - 1): + p_new[k] = unsafe_div( + price_scale[k] * unsafe_sub(norm, adjustment_step) + + adjustment_step * price_oracle[k], + norm + ) # <- norm is non-zero and gt adjustment_step; unsafe = safe + + # ---------------- Update stale xp (using price_scale) with p_new. + xp = _xp + for k in range(N_COINS - 1): + xp[k + 1] = unsafe_div(_xp[k + 1] * p_new[k], price_scale[k]) + # unsafe_div because we did safediv before ----^ + + # ------------------------------------------ Update D with new xp. + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + assert D > 0 # dev: unsafe D + # Check if calculated p_new is safu: + for k in range(N_COINS): + frac: uint256 = unsafe_div(xp[k] * 10**18, D) + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe p_new + + xp[0] = unsafe_div(D, N_COINS) + for k in range(N_COINS - 1): + xp[k + 1] = D * 10**18 / (N_COINS * p_new[k]) # <---- Convert + # xp to real prices. + + # ---------- Calculate new virtual_price using new xp and D. Reuse + # `old_virtual_price` (but it has new virtual_price). + old_virtual_price = unsafe_div( + 10**18 * MATH.geometric_mean(xp), total_supply + ) # <----- unsafe_div because we did safediv before (if vp>1e18) + + # ---------------------------- Proceed if we've got enough profit. + if ( + old_virtual_price > 10**18 and + 2 * old_virtual_price - 10**18 > xcp_profit + ): + + packed_price_scale = self._pack_prices(p_new) + + self.D = D + self.virtual_price = old_virtual_price + self.price_scale_packed = packed_price_scale + + return packed_price_scale + + # --------- price_scale was not adjusted. Update the profit counter and D. + self.D = D_unadjusted + self.virtual_price = virtual_price + + return packed_price_scale + + +@internal +def _claim_admin_fees(): + """ + @notice Claims admin fees and sends it to fee_receiver set in the factory. + @dev Functionally similar to: + 1. Calculating admin's share of fees, + 2. minting LP tokens, + 3. admin claims underlying tokens via remove_liquidity. + """ + + # --------------------- Check if fees can be claimed --------------------- + + # Disable fee claiming if: + # 1. If time passed since last fee claim is less than + # MIN_ADMIN_FEE_CLAIM_INTERVAL. + # 2. Pool parameters are being ramped. + + last_claim_time: uint256 = self.last_admin_fee_claim_timestamp + if ( + unsafe_sub(block.timestamp, last_claim_time) < MIN_ADMIN_FEE_CLAIM_INTERVAL or + self.future_A_gamma_time > block.timestamp + ): + return + + xcp_profit: uint256 = self.xcp_profit # <---------- Current pool profits. + xcp_profit_a: uint256 = self.xcp_profit_a # <- Profits at previous claim. + current_lp_token_supply: uint256 = self.totalSupply + + # Do not claim admin fees if: + # 1. insufficient profits accrued since last claim, and + # 2. there are less than 10**18 (or 1 unit of) lp tokens, else it can lead + # to manipulated virtual prices. + + if xcp_profit <= xcp_profit_a or current_lp_token_supply < 10**18: + return + + # ---------- Conditions met to claim admin fees: compute state. ---------- + + A_gamma: uint256[2] = self._A_gamma() + D: uint256 = self.D + vprice: uint256 = self.virtual_price + packed_price_scale: uint256 = self.price_scale_packed + fee_receiver: address = factory.fee_receiver() + balances: uint256[N_COINS] = self.balances + + # Admin fees are calculated as follows. + # 1. Calculate accrued profit since last claim. `xcp_profit` + # is the current profits. `xcp_profit_a` is the profits + # at the previous claim. + # 2. Take out admin's share, which is hardcoded at 5 * 10**9. + # (50% => half of 100% => 10**10 / 2 => 5 * 10**9). + # 3. Since half of the profits go to rebalancing the pool, we + # are left with half; so divide by 2. + + fees: uint256 = unsafe_div( + unsafe_sub(xcp_profit, xcp_profit_a) * ADMIN_FEE, 2 * 10**10 + ) + + # ------------------------------ Claim admin fees by minting admin's share + # of the pool in LP tokens. + + # This is the admin fee tokens claimed in self.add_liquidity. We add it to + # the LP token share that the admin needs to claim: + admin_share: uint256 = self.admin_lp_virtual_balance + frac: uint256 = 0 + if fee_receiver != empty(address) and fees > 0: + + # -------------------------------- Calculate admin share to be minted. + frac = vprice * 10**18 / (vprice - fees) - 10**18 + admin_share += current_lp_token_supply * frac / 10**18 + + # ------ Subtract fees from profits that will be used for rebalancing. + xcp_profit -= fees * 2 + + # ------------------- Recalculate virtual_price following admin fee claim. + total_supply_including_admin_share: uint256 = ( + current_lp_token_supply + admin_share + ) + vprice = ( + 10**18 * self.get_xcp(D, packed_price_scale) / + total_supply_including_admin_share + ) + + # Do not claim fees if doing so causes virtual price to drop below 10**18. + if vprice < 10**18: + return + + # ---------------------------- Update State ------------------------------ + + # Set admin virtual LP balances to zero because we claimed: + self.admin_lp_virtual_balance = 0 + + self.xcp_profit = xcp_profit + self.last_admin_fee_claim_timestamp = block.timestamp + + # Since we reduce balances: virtual price goes down + self.virtual_price = vprice + + # Adjust D after admin seemingly removes liquidity + self.D = D - unsafe_div(D * admin_share, total_supply_including_admin_share) + + if xcp_profit > xcp_profit_a: + self.xcp_profit_a = xcp_profit # <-------- Cache last claimed profit. + + # --------------------------- Handle Transfers --------------------------- + + admin_tokens: uint256[N_COINS] = empty(uint256[N_COINS]) + if admin_share > 0: + + for i in range(N_COINS): + + admin_tokens[i] = ( + balances[i] * admin_share / + total_supply_including_admin_share + ) + + # _transfer_out tokens to admin and update self.balances. State + # update to self.balances occurs before external contract calls: + self._transfer_out(i, admin_tokens[i], fee_receiver) + + log ClaimAdminFee(fee_receiver, admin_tokens) + + +@internal +@pure +def xp( + balances: uint256[N_COINS], + price_scale_packed: uint256, +) -> uint256[N_COINS]: + + result: uint256[N_COINS] = balances + result[0] *= PRECISIONS[0] + packed_prices: uint256 = price_scale_packed + for i in range(1, N_COINS): + p: uint256 = (packed_prices & PRICE_MASK) * PRECISIONS[i] + result[i] = result[i] * p / PRECISION + packed_prices = packed_prices >> PRICE_SIZE + + return result + + +@internal +@view +def _alpha(last_timestamp: uint256, ma_exp_time: uint256) -> uint256: + + return MATH.wad_exp( + -convert( + unsafe_div( + (block.timestamp - last_timestamp) * 10**18, + ma_exp_time + ), + int256, + ) + ) + + +@view +@internal +def _A_gamma() -> uint256[2]: + t1: uint256 = self.future_A_gamma_time + + A_gamma_1: uint256 = self.future_A_gamma + gamma1: uint256 = A_gamma_1 & 2**128 - 1 + A1: uint256 = A_gamma_1 >> 128 + + if block.timestamp < t1: + + # --------------- Handle ramping up and down of A -------------------- + + A_gamma_0: uint256 = self.initial_A_gamma + t0: uint256 = self.initial_A_gamma_time + + t1 -= t0 + t0 = block.timestamp - t0 + t2: uint256 = t1 - t0 + + A1 = ((A_gamma_0 >> 128) * t2 + A1 * t0) / t1 + gamma1 = ((A_gamma_0 & 2**128 - 1) * t2 + gamma1 * t0) / t1 + + return [A1, gamma1] + + +@internal +@view +def _fee(xp: uint256[N_COINS]) -> uint256: + + fee_params: uint256[3] = self._unpack_3(self.packed_fee_params) + f: uint256 = MATH.reduction_coefficient(xp, fee_params[2]) + + return unsafe_div( + fee_params[0] * f + fee_params[1] * (10**18 - f), + 10**18 + ) + + +@internal +@pure +def get_xcp(D: uint256, price_scale_packed: uint256) -> uint256: + + x: uint256[N_COINS] = empty(uint256[N_COINS]) + x[0] = D / N_COINS + packed_prices: uint256 = price_scale_packed # <------ No precisions here + # because we don't switch to "real" units. + + for i in range(1, N_COINS): + x[i] = D * 10**18 / (N_COINS * (packed_prices & PRICE_MASK)) + packed_prices = packed_prices >> PRICE_SIZE + + return MATH.geometric_mean(x) + + +@view +@internal +def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256: + # fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts) + fee: uint256 = unsafe_div( + unsafe_mul(self._fee(xp), N_COINS), + unsafe_mul(4, unsafe_sub(N_COINS, 1)) + ) + + S: uint256 = 0 + for _x in amounts: + S += _x + + avg: uint256 = unsafe_div(S, N_COINS) + Sdiff: uint256 = 0 + + for _x in amounts: + if _x > avg: + Sdiff += unsafe_sub(_x, avg) + else: + Sdiff += unsafe_sub(avg, _x) + + return fee * Sdiff / S + NOISE_FEE + + +@internal +@view +def _calc_withdraw_one_coin( + A_gamma: uint256[2], + token_amount: uint256, + i: uint256, + update_D: bool, +) -> (uint256, uint256, uint256[N_COINS], uint256): + + token_supply: uint256 = self.totalSupply + assert token_amount <= token_supply # dev: token amount more than supply + assert i < N_COINS # dev: coin out of range + + xx: uint256[N_COINS] = self.balances + xp: uint256[N_COINS] = PRECISIONS + D0: uint256 = 0 + + # -------------------------- Calculate D0 and xp ------------------------- + + price_scale_i: uint256 = PRECISION * PRECISIONS[0] + packed_prices: uint256 = self.price_scale_packed + xp[0] *= xx[0] + for k in range(1, N_COINS): + p: uint256 = (packed_prices & PRICE_MASK) + if i == k: + price_scale_i = p * xp[i] + xp[k] = unsafe_div(xp[k] * xx[k] * p, PRECISION) + packed_prices = packed_prices >> PRICE_SIZE + + if update_D: # <-------------- D is updated if pool is undergoing a ramp. + D0 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + else: + D0 = self.D + + D: uint256 = D0 + + # -------------------------------- Fee Calc ------------------------------ + + # Charge fees on D. Roughly calculate xp[i] after withdrawal and use that + # to calculate fee. Precision is not paramount here: we just want a + # behavior where the higher the imbalance caused the more fee the AMM + # charges. + + # xp is adjusted assuming xp[0] ~= xp[1] ~= x[2], which is usually not the + # case. We charge self._fee(xp), where xp is an imprecise adjustment post + # withdrawal in one coin. If the withdraw is too large: charge max fee by + # default. This is because the fee calculation will otherwise underflow. + + xp_imprecise: uint256[N_COINS] = xp + xp_correction: uint256 = xp[i] * N_COINS * token_amount / token_supply + fee: uint256 = self._unpack_3(self.packed_fee_params)[1] # <- self.out_fee. + + if xp_correction < xp_imprecise[i]: + xp_imprecise[i] -= xp_correction + fee = self._fee(xp_imprecise) + + dD: uint256 = unsafe_div(token_amount * D, token_supply) + D_fee: uint256 = fee * dD / (2 * 10**10) + 1 # <------- Actual fee on D. + + # --------- Calculate `approx_fee` (assuming balanced state) in ith token. + # -------------------------------- We only need this for fee in the event. + approx_fee: uint256 = N_COINS * D_fee * xx[i] / D + + # ------------------------------------------------------------------------ + D -= (dD - D_fee) # <----------------------------------- Charge fee on D. + # --------------------------------- Calculate `y_out`` with `(D - D_fee)`. + y: uint256 = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, i)[0] + dy: uint256 = (xp[i] - y) * PRECISION / price_scale_i + xp[i] = y + + return dy, D, xp, approx_fee + + +# ------------------------ ERC20 functions ----------------------------------- + + +@internal +def _approve(_owner: address, _spender: address, _value: uint256): + self.allowance[_owner][_spender] = _value + + log Approval(_owner, _spender, _value) + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + assert _to not in [self, empty(address)] + + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + + log Transfer(_from, _to, _value) + + +@view +@internal +def _domain_separator() -> bytes32: + if chain.id != CACHED_CHAIN_ID: + return keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + return CACHED_DOMAIN_SEPARATOR + + +@external +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + """ + @dev Transfer tokens from one address to another. + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + @return bool True on successul transfer. Reverts otherwise. + """ + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != max_value(uint256): + self._approve(_from, msg.sender, _allowance - _value) + + self._transfer(_from, _to, _value) + return True + + +@external +def transfer(_to: address, _value: uint256) -> bool: + """ + @dev Transfer token for a specified address + @param _to The address to transfer to. + @param _value The amount to be transferred. + @return bool True on successful transfer. Reverts otherwise. + """ + self._transfer(msg.sender, _to, _value) + return True + + +@external +def approve(_spender: address, _value: uint256) -> bool: + """ + @notice Allow `_spender` to transfer up to `_value` amount + of tokens from the caller's account. + @param _spender The account permitted to spend up to `_value` amount of + caller's funds. + @param _value The amount of tokens `_spender` is allowed to spend. + @return bool Success + """ + self._approve(msg.sender, _spender, _value) + return True + + +@external +def permit( + _owner: address, + _spender: address, + _value: uint256, + _deadline: uint256, + _v: uint8, + _r: bytes32, + _s: bytes32, +) -> bool: + """ + @notice Permit `_spender` to spend up to `_value` amount of `_owner`'s + tokens via a signature. + @dev In the event of a chain fork, replay attacks are prevented as + domain separator is recalculated. However, this is only if the + resulting chains update their chainId. + @param _owner The account which generated the signature and is granting an + allowance. + @param _spender The account which will be granted an allowance. + @param _value The approval amount. + @param _deadline The deadline by which the signature must be submitted. + @param _v The last byte of the ECDSA signature. + @param _r The first 32 bytes of the ECDSA signature. + @param _s The second 32 bytes of the ECDSA signature. + @return bool Success. + """ + assert _owner != empty(address) # dev: invalid owner + assert block.timestamp <= _deadline # dev: permit expired + + nonce: uint256 = self.nonces[_owner] + digest: bytes32 = keccak256( + concat( + b"\x19\x01", + self._domain_separator(), + keccak256( + _abi_encode( + EIP2612_TYPEHASH, _owner, _spender, _value, nonce, _deadline + ) + ), + ) + ) + assert ecrecover(digest, _v, _r, _s) == _owner # dev: invalid signature + + self.nonces[_owner] = unsafe_add(nonce, 1) # <-- Unsafe add is safe here. + self._approve(_owner, _spender, _value) + return True + + +@internal +def mint(_to: address, _value: uint256) -> bool: + """ + @dev Mint an amount of the token and assigns it to an account. + This encapsulates the modification of balances such that the + proper events are emitted. + @param _to The account that will receive the created tokens. + @param _value The amount that will be created. + @return bool Success. + """ + self.totalSupply += _value + self.balanceOf[_to] += _value + + log Transfer(empty(address), _to, _value) + return True + + +@internal +def burnFrom(_to: address, _value: uint256) -> bool: + """ + @dev Burn an amount of the token from a given account. + @param _to The account whose tokens will be burned. + @param _value The amount that will be burned. + @return bool Success. + """ + self.totalSupply -= _value + self.balanceOf[_to] -= _value + + log Transfer(_to, empty(address), _value) + return True + + +# ------------------------- AMM View Functions ------------------------------- + + +@external +@view +def fee_receiver() -> address: + """ + @notice Returns the address of the admin fee receiver. + @return address Fee receiver. + """ + return factory.fee_receiver() + + +@external +@view +def admin() -> address: + """ + @notice Returns the address of the pool's admin. + @return address Admin. + """ + return factory.admin() + + +@external +@view +def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256: + """ + @notice Calculate LP tokens minted or to be burned for depositing or + removing `amounts` of coins + @dev Includes fee. + @param amounts Amounts of tokens being deposited or withdrawn + @param deposit True if it is a deposit action, False if withdrawn. + @return uint256 Amount of LP tokens deposited or withdrawn. + """ + view_contract: address = factory.views_implementation() + return Views(view_contract).calc_token_amount(amounts, deposit, self) + + +@external +@view +def get_dy(i: uint256, j: uint256, dx: uint256) -> uint256: + """ + @notice Get amount of coin[j] tokens received for swapping in dx amount of coin[i] + @dev Includes fee. + @param i index of input token. Check pool.coins(i) to get coin address at ith index + @param j index of output token + @param dx amount of input coin[i] tokens + @return uint256 Exact amount of output j tokens for dx amount of i input tokens. + """ + view_contract: address = factory.views_implementation() + return Views(view_contract).get_dy(i, j, dx, self) + + +@external +@view +def get_dx(i: uint256, j: uint256, dy: uint256) -> uint256: + """ + @notice Get amount of coin[i] tokens to input for swapping out dy amount + of coin[j] + @dev This is an approximate method, and returns estimates close to the input + amount. Expensive to call on-chain. + @param i index of input token. Check pool.coins(i) to get coin address at + ith index + @param j index of output token + @param dy amount of input coin[j] tokens received + @return uint256 Approximate amount of input i tokens to get dy amount of j tokens. + """ + view_contract: address = factory.views_implementation() + return Views(view_contract).get_dx(i, j, dy, self) + + +@external +@view +@nonreentrant("lock") +def lp_price() -> uint256: + """ + @notice Calculates the current price of the LP token w.r.t coin at the + 0th index + @return uint256 LP price. + """ + + price_oracle: uint256[N_COINS-1] = self._unpack_prices(self.price_oracle_packed) + return ( + 3 * self.virtual_price * MATH.cbrt(price_oracle[0] * price_oracle[1]) + ) / 10**24 + + +@external +@view +@nonreentrant("lock") +def get_virtual_price() -> uint256: + """ + @notice Calculates the current virtual price of the pool LP token. + @dev Not to be confused with `self.virtual_price` which is a cached + virtual price. + @return uint256 Virtual Price. + """ + return ( + 10**18 * self.get_xcp(self.D, self.price_scale_packed) / + self.totalSupply + ) + + +@external +@view +@nonreentrant("lock") +def price_oracle(k: uint256) -> uint256: + """ + @notice Returns the oracle price of the coin at index `k` w.r.t the coin + at index 0. + @dev The oracle is an exponential moving average, with a periodicity + determined by `self.ma_time`. The aggregated prices are cached state + prices (dy/dx) calculated AFTER the latest trade. + State prices that goes into the EMA are capped at 2 x price_scale. + @param k The index of the coin. + @return uint256 Price oracle value of kth coin. + """ + price_oracle: uint256 = self._unpack_prices(self.price_oracle_packed)[k] + price_scale: uint256 = self._unpack_prices(self.price_scale_packed)[k] + last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[0] + + if last_prices_timestamp < block.timestamp: # <------------ Update moving + # average if needed. + + last_prices: uint256 = self._unpack_prices(self.last_prices_packed)[k] + ma_time: uint256 = self._unpack_3(self.packed_rebalancing_params)[2] + alpha: uint256 = self._alpha(last_prices_timestamp, ma_time) + return ( + min(last_prices, 2 * price_scale) * (10**18 - alpha) + + price_oracle * alpha + ) / 10**18 + + return price_oracle + + +@external +@view +@nonreentrant("lock") +def xcp_oracle() -> uint256: + """ + @notice Returns the oracle value for xcp. + @dev The oracle is an exponential moving average, with a periodicity + determined by `self.xcp_ma_time`. + `TVL` is xcp, calculated as either: + 1. virtual_price * total_supply, OR + 2. self.get_xcp(...), OR + 3. MATH.geometric_mean(xp) + @return uint256 Oracle value of xcp. + """ + + last_xcp_timestamp: uint256 = self._unpack_2(self.last_timestamp)[1] + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + + if last_xcp_timestamp < block.timestamp: + + alpha: uint256 = self._alpha(last_xcp_timestamp, self.xcp_ma_time) + return (self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha) / 10**18 + + return cached_xcp_oracle + + +@external +@view +def last_prices(k: uint256) -> uint256: + """ + @notice Returns last price of the coin at index `k` w.r.t the coin + at index 0. + @dev last_prices returns the quote by the AMM for an infinitesimally small swap + after the last trade. It is not equivalent to the last traded price, and + is computed by taking the partial differential of `x` w.r.t `y`. The + derivative is calculated in `get_p` and then multiplied with price_scale + to give last_prices. + @param k The index of the coin. + @return uint256 Last logged price of coin. + """ + return self._unpack_prices(self.last_prices_packed)[k] + + +@external +@view +@nonreentrant("lock") +def price_scale(k: uint256) -> uint256: + """ + @notice Returns the price scale of the coin at index `k` w.r.t the coin + at index 0. + @dev Price scale determines the price band around which liquidity is + concentrated. + @param k The index of the coin. + @return uint256 Price scale of coin. + """ + return self._unpack_prices(self.price_scale_packed)[k] + + +@external +@view +def fee() -> uint256: + """ + @notice Returns the fee charged by the pool at current state. + @dev Not to be confused with the fee charged at liquidity action, since + there the fee is calculated on `xp` AFTER liquidity is added or + removed. + @return uint256 fee bps. + """ + return self._fee(self.xp(self.balances, self.price_scale_packed)) + + +@view +@external +def calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256: + """ + @notice Calculates output tokens with fee + @param token_amount LP Token amount to burn + @param i token in which liquidity is withdrawn + @return uint256 Amount of ith tokens received for burning token_amount LP tokens. + """ + + return self._calc_withdraw_one_coin( + self._A_gamma(), + token_amount, + i, + (self.future_A_gamma_time > block.timestamp) + )[0] + + +@external +@view +def calc_token_fee( + amounts: uint256[N_COINS], xp: uint256[N_COINS] +) -> uint256: + """ + @notice Returns the fee charged on the given amounts for add_liquidity. + @param amounts The amounts of coins being added to the pool. + @param xp The current balances of the pool multiplied by coin precisions. + @return uint256 Fee charged. + """ + return self._calc_token_fee(amounts, xp) + + +@view +@external +def A() -> uint256: + """ + @notice Returns the current pool amplification parameter. + @return uint256 A param. + """ + return self._A_gamma()[0] + + +@view +@external +def gamma() -> uint256: + """ + @notice Returns the current pool gamma parameter. + @return uint256 gamma param. + """ + return self._A_gamma()[1] + + +@view +@external +def mid_fee() -> uint256: + """ + @notice Returns the current mid fee + @return uint256 mid_fee value. + """ + return self._unpack_3(self.packed_fee_params)[0] + + +@view +@external +def out_fee() -> uint256: + """ + @notice Returns the current out fee + @return uint256 out_fee value. + """ + return self._unpack_3(self.packed_fee_params)[1] + + +@view +@external +def fee_gamma() -> uint256: + """ + @notice Returns the current fee gamma + @return uint256 fee_gamma value. + """ + return self._unpack_3(self.packed_fee_params)[2] + + +@view +@external +def allowed_extra_profit() -> uint256: + """ + @notice Returns the current allowed extra profit + @return uint256 allowed_extra_profit value. + """ + return self._unpack_3(self.packed_rebalancing_params)[0] + + +@view +@external +def adjustment_step() -> uint256: + """ + @notice Returns the current adjustment step + @return uint256 adjustment_step value. + """ + return self._unpack_3(self.packed_rebalancing_params)[1] + + +@view +@external +def ma_time() -> uint256: + """ + @notice Returns the current moving average time in seconds + @dev To get time in seconds, the parameter is multipled by ln(2) + One can expect off-by-one errors here. + @return uint256 ma_time value. + """ + return self._unpack_3(self.packed_rebalancing_params)[2] * 694 / 1000 + + +@view +@external +def precisions() -> uint256[N_COINS]: # <-------------- For by view contract. + """ + @notice Returns the precisions of each coin in the pool. + @return uint256[3] precisions of coins. + """ + return PRECISIONS + + +@external +@view +def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract. + """ + @notice Returns the fee charged by the pool at current state. + @param xp The current balances of the pool multiplied by coin precisions. + @return uint256 Fee value. + """ + return self._fee(xp) + + +@view +@external +def DOMAIN_SEPARATOR() -> bytes32: + """ + @notice EIP712 domain separator. + @return bytes32 Domain Separator set for the current chain. + """ + return self._domain_separator() + + +# ------------------------- AMM Admin Functions ------------------------------ + + +@external +def ramp_A_gamma( + future_A: uint256, future_gamma: uint256, future_time: uint256 +): + """ + @notice Initialise Ramping A and gamma parameter values linearly. + @dev Only accessible by factory admin, and only + @param future_A The future A value. + @param future_gamma The future gamma value. + @param future_time The timestamp at which the ramping will end. + """ + assert msg.sender == factory.admin() # dev: only owner + assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing + assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time + + A_gamma: uint256[2] = self._A_gamma() + initial_A_gamma: uint256 = A_gamma[0] << 128 + initial_A_gamma = initial_A_gamma | A_gamma[1] + + assert future_A > MIN_A - 1 + assert future_A < MAX_A + 1 + assert future_gamma > MIN_GAMMA - 1 + assert future_gamma < MAX_GAMMA + 1 + + ratio: uint256 = 10**18 * future_A / A_gamma[0] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + ratio = 10**18 * future_gamma / A_gamma[1] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + self.initial_A_gamma = initial_A_gamma + self.initial_A_gamma_time = block.timestamp + + future_A_gamma: uint256 = future_A << 128 + future_A_gamma = future_A_gamma | future_gamma + self.future_A_gamma_time = future_time + self.future_A_gamma = future_A_gamma + + log RampAgamma( + A_gamma[0], + future_A, + A_gamma[1], + future_gamma, + block.timestamp, + future_time, + ) + + +@external +def stop_ramp_A_gamma(): + """ + @notice Stop Ramping A and gamma parameters immediately. + @dev Only accessible by factory admin. + """ + assert msg.sender == factory.admin() # dev: only owner + + A_gamma: uint256[2] = self._A_gamma() + current_A_gamma: uint256 = A_gamma[0] << 128 + current_A_gamma = current_A_gamma | A_gamma[1] + self.initial_A_gamma = current_A_gamma + self.future_A_gamma = current_A_gamma + self.initial_A_gamma_time = block.timestamp + self.future_A_gamma_time = block.timestamp + + # ------ Now (block.timestamp < t1) is always False, so we return saved A. + + log StopRampA(A_gamma[0], A_gamma[1], block.timestamp) + + +@external +@nonreentrant('lock') +def apply_new_parameters( + _new_mid_fee: uint256, + _new_out_fee: uint256, + _new_fee_gamma: uint256, + _new_allowed_extra_profit: uint256, + _new_adjustment_step: uint256, + _new_ma_time: uint256, + _new_xcp_ma_time: uint256, +): + """ + @notice Commit new parameters. + @dev Only accessible by factory admin. + @param _new_mid_fee The new mid fee. + @param _new_out_fee The new out fee. + @param _new_fee_gamma The new fee gamma. + @param _new_allowed_extra_profit The new allowed extra profit. + @param _new_adjustment_step The new adjustment step. + @param _new_ma_time The new ma time. ma_time is time_in_seconds/ln(2). + @param _new_xcp_ma_time The new ma time for xcp oracle. + """ + assert msg.sender == factory.admin() # dev: only owner + + # ----------------------------- Set fee params --------------------------- + + new_mid_fee: uint256 = _new_mid_fee + new_out_fee: uint256 = _new_out_fee + new_fee_gamma: uint256 = _new_fee_gamma + + current_fee_params: uint256[3] = self._unpack_3(self.packed_fee_params) + + if new_out_fee < MAX_FEE + 1: + assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range + else: + new_out_fee = current_fee_params[1] + + if new_mid_fee > MAX_FEE: + new_mid_fee = current_fee_params[0] + assert new_mid_fee <= new_out_fee # dev: mid-fee is too high + + if new_fee_gamma < 10**18: + assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18] + else: + new_fee_gamma = current_fee_params[2] + + self.packed_fee_params = self._pack_3([new_mid_fee, new_out_fee, new_fee_gamma]) + + # ----------------- Set liquidity rebalancing parameters ----------------- + + new_allowed_extra_profit: uint256 = _new_allowed_extra_profit + new_adjustment_step: uint256 = _new_adjustment_step + new_ma_time: uint256 = _new_ma_time + + current_rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params) + + if new_allowed_extra_profit > 10**18: + new_allowed_extra_profit = current_rebalancing_params[0] + + if new_adjustment_step > 10**18: + new_adjustment_step = current_rebalancing_params[1] + + if new_ma_time < 872542: # <----- Calculated as: 7 * 24 * 60 * 60 / ln(2) + assert new_ma_time > 86 # dev: MA time should be longer than 60/ln(2) + else: + new_ma_time = current_rebalancing_params[2] + + self.packed_rebalancing_params = self._pack_3( + [new_allowed_extra_profit, new_adjustment_step, new_ma_time] + ) + + # Set xcp oracle moving average window time: + new_xcp_ma_time: uint256 = _new_xcp_ma_time + if new_xcp_ma_time < 872542: + assert new_xcp_ma_time > 86 # dev: xcp MA time should be longer than 60/ln(2) + else: + new_xcp_ma_time = self.xcp_ma_time + self.xcp_ma_time = new_xcp_ma_time + + # ---------------------------------- LOG --------------------------------- + + log NewParameters( + new_mid_fee, + new_out_fee, + new_fee_gamma, + new_allowed_extra_profit, + new_adjustment_step, + new_ma_time, + _new_xcp_ma_time, + ) diff --git a/contracts/amms/tricryptong/CurveTricryptoOptimizedWETH.vy b/contracts/amms/tricryptong/CurveTricryptoOptimizedWETH.vy new file mode 100644 index 0000000..d1bf61f --- /dev/null +++ b/contracts/amms/tricryptong/CurveTricryptoOptimizedWETH.vy @@ -0,0 +1,2158 @@ +# pragma version 0.3.10 +# pragma optimize gas +# pragma evm-version paris +""" +@title CurveTricryptoOptimizedWETH +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice A Curve AMM pool for 3 unpegged assets (e.g. ETH, BTC, USD). +@dev All prices in the AMM are with respect to the first token in the pool. + Supports native token transfers. +""" + +from vyper.interfaces import ERC20 +implements: ERC20 # <--------------------- AMM contract is also the LP token. + +# --------------------------------- Interfaces ------------------------------- + +interface Math: + def geometric_mean(_x: uint256[N_COINS]) -> uint256: view + def wad_exp(_power: int256) -> uint256: view + def cbrt(x: uint256) -> uint256: view + def reduction_coefficient( + x: uint256[N_COINS], fee_gamma: uint256 + ) -> uint256: view + def newton_D( + ANN: uint256, + gamma: uint256, + x_unsorted: uint256[N_COINS], + K0_prev: uint256 + ) -> uint256: view + def get_y( + ANN: uint256, + gamma: uint256, + x: uint256[N_COINS], + D: uint256, + i: uint256, + ) -> uint256[2]: view + def get_p( + _xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[2], + ) -> uint256[N_COINS-1]: view + +interface WETH: + def deposit(): payable + def withdraw(_amount: uint256): nonpayable + +interface Factory: + def admin() -> address: view + def fee_receiver() -> address: view + def views_implementation() -> address: view + +interface Views: + def calc_token_amount( + amounts: uint256[N_COINS], deposit: bool, swap: address + ) -> uint256: view + def get_dy( + i: uint256, j: uint256, dx: uint256, swap: address + ) -> uint256: view + def get_dx( + i: uint256, j: uint256, dy: uint256, swap: address + ) -> uint256: view + + +# ------------------------------- Events ------------------------------------- + +event Transfer: + sender: indexed(address) + receiver: indexed(address) + value: uint256 + +event Approval: + owner: indexed(address) + spender: indexed(address) + value: uint256 + +event TokenExchange: + buyer: indexed(address) + sold_id: uint256 + tokens_sold: uint256 + bought_id: uint256 + tokens_bought: uint256 + fee: uint256 + packed_price_scale: uint256 + +event AddLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fee: uint256 + token_supply: uint256 + packed_price_scale: uint256 + +event RemoveLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + token_supply: uint256 + +event RemoveLiquidityOne: + provider: indexed(address) + token_amount: uint256 + coin_index: uint256 + coin_amount: uint256 + approx_fee: uint256 + packed_price_scale: uint256 + +event CommitNewParameters: + deadline: indexed(uint256) + mid_fee: uint256 + out_fee: uint256 + fee_gamma: uint256 + allowed_extra_profit: uint256 + adjustment_step: uint256 + ma_time: uint256 + +event NewParameters: + mid_fee: uint256 + out_fee: uint256 + fee_gamma: uint256 + allowed_extra_profit: uint256 + adjustment_step: uint256 + ma_time: uint256 + +event RampAgamma: + initial_A: uint256 + future_A: uint256 + initial_gamma: uint256 + future_gamma: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + current_A: uint256 + current_gamma: uint256 + time: uint256 + +event ClaimAdminFee: + admin: indexed(address) + tokens: uint256 + + +# ----------------------- Storage/State Variables ---------------------------- + +WETH20: public(immutable(address)) + +N_COINS: constant(uint256) = 3 +PRECISION: constant(uint256) = 10**18 # <------- The precision to convert to. +A_MULTIPLIER: constant(uint256) = 10000 +packed_precisions: uint256 + +MATH: public(immutable(Math)) +coins: public(immutable(address[N_COINS])) +factory: public(address) + +price_scale_packed: uint256 # <------------------------ Internal price scale. +price_oracle_packed: uint256 # <------- Price target given by moving average. + +last_prices_packed: uint256 +last_prices_timestamp: public(uint256) + +initial_A_gamma: public(uint256) +initial_A_gamma_time: public(uint256) + +future_A_gamma: public(uint256) +future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. +# This value is 0 (default) when pool is first deployed, and only gets +# populated by block.timestamp + future_time in `ramp_A_gamma` when the +# ramping process is initiated. After ramping is finished +# (i.e. self.future_A_gamma_time < block.timestamp), the variable is left +# and not set to 0. + +balances: public(uint256[N_COINS]) +D: public(uint256) +xcp_profit: public(uint256) +xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees. + +virtual_price: public(uint256) # <------ Cached (fast to read) virtual price. +# The cached `virtual_price` is also used internally. + +# -------------- Params that affect how price_scale get adjusted ------------- + +packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing +# parameters allowed_extra_profit, adjustment_step, and ma_time. + +future_packed_rebalancing_params: uint256 + +# ---------------- Fee params that determine dynamic fees -------------------- + +packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma. +future_packed_fee_params: uint256 + +ADMIN_FEE: public(constant(uint256)) = 5 * 10**9 # <----- 50% of earned fees. +MIN_FEE: constant(uint256) = 5 * 10**5 # <-------------------------- 0.5 BPS. +MAX_FEE: constant(uint256) = 10 * 10**9 +NOISE_FEE: constant(uint256) = 10**5 # <---------------------------- 0.1 BPS. + +# ----------------------- Admin params --------------------------------------- + +admin_actions_deadline: public(uint256) + +ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 +MIN_RAMP_TIME: constant(uint256) = 86400 + +MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 100 +MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS +MAX_A_CHANGE: constant(uint256) = 10 +MIN_GAMMA: constant(uint256) = 10**10 +MAX_GAMMA: constant(uint256) = 5 * 10**16 + +PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1) +PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1 + +# ----------------------- ERC20 Specific vars -------------------------------- + +name: public(immutable(String[64])) +symbol: public(immutable(String[32])) +decimals: public(constant(uint8)) = 18 +version: public(constant(String[8])) = "v2.0.0" + +balanceOf: public(HashMap[address, uint256]) +allowance: public(HashMap[address, HashMap[address, uint256]]) +totalSupply: public(uint256) +nonces: public(HashMap[address, uint256]) + +EIP712_TYPEHASH: constant(bytes32) = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" +) +EIP2612_TYPEHASH: constant(bytes32) = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +) +VERSION_HASH: constant(bytes32) = keccak256(version) +NAME_HASH: immutable(bytes32) +CACHED_CHAIN_ID: immutable(uint256) +salt: public(immutable(bytes32)) +CACHED_DOMAIN_SEPARATOR: immutable(bytes32) + + +# ----------------------- Contract ------------------------------------------- + +@external +def __init__( + _name: String[64], + _symbol: String[32], + _coins: address[N_COINS], + _math: address, + _weth: address, + _salt: bytes32, + packed_precisions: uint256, + packed_A_gamma: uint256, + packed_fee_params: uint256, + packed_rebalancing_params: uint256, + packed_prices: uint256, +): + + WETH20 = _weth + MATH = Math(_math) + + self.factory = msg.sender + + name = _name + symbol = _symbol + coins = _coins + + self.packed_precisions = packed_precisions # <------- Precisions of coins + # are calculated as 10**(18 - coin.decimals()). + + self.initial_A_gamma = packed_A_gamma # <------------------- A and gamma. + self.future_A_gamma = packed_A_gamma + + self.packed_rebalancing_params = packed_rebalancing_params # <-- Contains + # rebalancing params: allowed_extra_profit, adjustment_step, + # and ma_exp_time. + + self.packed_fee_params = packed_fee_params # <-------------- Contains Fee + # params: mid_fee, out_fee and fee_gamma. + + self.price_scale_packed = packed_prices + self.price_oracle_packed = packed_prices + self.last_prices_packed = packed_prices + self.last_prices_timestamp = block.timestamp + self.xcp_profit_a = 10**18 + + # Cache DOMAIN_SEPARATOR. If chain.id is not CACHED_CHAIN_ID, then + # DOMAIN_SEPARATOR will be re-calculated each time `permit` is called. + # Otherwise, it will always use CACHED_DOMAIN_SEPARATOR. + # see: `_domain_separator()` for its implementation. + NAME_HASH = keccak256(name) + salt = _salt + CACHED_CHAIN_ID = chain.id + CACHED_DOMAIN_SEPARATOR = keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + + log Transfer(empty(address), self, 0) # <------- Fire empty transfer from + # 0x0 to self for indexers to catch. + + +# ------------------- Token transfers in and out of the AMM ------------------ + + +@payable +@external +def __default__(): + if msg.value > 0: + assert WETH20 in coins + + +@internal +def _transfer_in( + _coin: address, + dx: uint256, + dy: uint256, + mvalue: uint256, + callbacker: address, + callback_sig: bytes32, + sender: address, + receiver: address, + use_eth: bool +): + """ + @notice Transfers `_coin` from `sender` to `self` and calls `callback_sig` + if it is not empty. + @dev The callback sig must have the following args: + sender: address + receiver: address + coin: address + dx: uint256 + dy: uint256 + @params _coin address of the coin to transfer in. + @params dx amount of `_coin` to transfer into the pool. + @params dy amount of `_coin` to transfer out of the pool. + @params mvalue msg.value if the transfer is ETH, 0 otherwise. + @params callbacker address to call `callback_sig` on. + @params callback_sig signature of the callback function. + @params sender address to transfer `_coin` from. + @params receiver address to transfer `_coin` to. + @params use_eth True if the transfer is ETH, False otherwise. + """ + + if use_eth and _coin == WETH20: + assert mvalue == dx # dev: incorrect eth amount + else: + assert mvalue == 0 # dev: nonzero eth amount + + if callback_sig == empty(bytes32): + + assert ERC20(_coin).transferFrom( + sender, self, dx, default_return_value=True + ) + + else: + + # --------- This part of the _transfer_in logic is only accessible + # by _exchange. + + # First call callback logic and then check if pool + # gets dx amounts of _coins[i], revert otherwise. + b: uint256 = ERC20(_coin).balanceOf(self) + raw_call( + callbacker, + concat( + slice(callback_sig, 0, 4), + _abi_encode(sender, receiver, _coin, dx, dy) + ) + ) + assert ERC20(_coin).balanceOf(self) - b == dx # dev: callback didn't give us coins + # ^------ note: dx cannot + # be 0, so the contract MUST receive some _coin. + + if _coin == WETH20: + WETH(WETH20).withdraw(dx) # <--------- if WETH was transferred in + # previous step and `not use_eth`, withdraw WETH to ETH. + + +@internal +def _transfer_out( + _coin: address, _amount: uint256, use_eth: bool, receiver: address +): + """ + @notice Transfer a single token from the pool to receiver. + @dev This function is called by `remove_liquidity` and + `remove_liquidity_one` and `_exchange` methods. + @params _coin Address of the token to transfer out + @params _amount Amount of token to transfer out + @params use_eth Whether to transfer ETH or not + @params receiver Address to send the tokens to + """ + + if use_eth and _coin == WETH20: + raw_call(receiver, b"", value=_amount) + else: + if _coin == WETH20: + WETH(WETH20).deposit(value=_amount) + + assert ERC20(_coin).transfer( + receiver, _amount, default_return_value=True + ) + + +# -------------------------- AMM Main Functions ------------------------------ + + +@payable +@external +@nonreentrant("lock") +def exchange( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + use_eth: bool = False, + receiver: address = msg.sender +) -> uint256: + """ + @notice Exchange using wrapped native token by default + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param use_eth True if the input coin is native token, False otherwise + @param receiver Address to send the output coin to. Default is msg.sender + @return uint256 Amount of tokens at index j received by the `receiver + """ + return self._exchange( + msg.sender, + msg.value, + i, + j, + dx, + min_dy, + use_eth, + receiver, + empty(address), + empty(bytes32) + ) + + +@payable +@external +@nonreentrant('lock') +def exchange_underlying( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + receiver: address = msg.sender +) -> uint256: + """ + @notice Exchange using native token transfers. + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param receiver Address to send the output coin to. Default is msg.sender + @return uint256 Amount of tokens at index j received by the `receiver + """ + return self._exchange( + msg.sender, + msg.value, + i, + j, + dx, + min_dy, + True, + receiver, + empty(address), + empty(bytes32) + ) + + +@external +@nonreentrant('lock') +def exchange_extended( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + use_eth: bool, + sender: address, + receiver: address, + cb: bytes32 +) -> uint256: + """ + @notice Exchange with callback method. + @dev This method does not allow swapping in native token, but does allow + swaps that transfer out native token from the pool. + @dev Does not allow flashloans + @dev One use-case is to reduce the number of redundant ERC20 token + transfers in zaps. + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param use_eth True if output is native token, False otherwise + @param sender Address to transfer input coin from + @param receiver Address to send the output coin to + @param cb Callback signature + @return uint256 Amount of tokens at index j received by the `receiver` + """ + + assert cb != empty(bytes32) # dev: No callback specified + return self._exchange( + sender, 0, i, j, dx, min_dy, use_eth, receiver, msg.sender, cb + ) # callbacker should never be self ------------------^ + + +@payable +@external +@nonreentrant("lock") +def add_liquidity( + amounts: uint256[N_COINS], + min_mint_amount: uint256, + use_eth: bool = False, + receiver: address = msg.sender +) -> uint256: + """ + @notice Adds liquidity into the pool. + @param amounts Amounts of each coin to add. + @param min_mint_amount Minimum amount of LP to mint. + @param use_eth True if native token is being added to the pool. + @param receiver Address to send the LP tokens to. Default is msg.sender + @return uint256 Amount of LP tokens received by the `receiver + """ + + A_gamma: uint256[2] = self._A_gamma() + xp: uint256[N_COINS] = self.balances + amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) + xx: uint256[N_COINS] = empty(uint256[N_COINS]) + d_token: uint256 = 0 + d_token_fee: uint256 = 0 + old_D: uint256 = 0 + + assert amounts[0] + amounts[1] + amounts[2] > 0 # dev: no coins to add + + # --------------------- Get prices, balances ----------------------------- + + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS-1] = self._unpack_prices(packed_price_scale) + + # -------------------------------------- Update balances and calculate xp. + xp_old: uint256[N_COINS] = xp + for i in range(N_COINS): + bal: uint256 = xp[i] + amounts[i] + xp[i] = bal + self.balances[i] = bal + xx = xp + + xp[0] *= precisions[0] + xp_old[0] *= precisions[0] + for i in range(1, N_COINS): + xp[i] = unsafe_div(xp[i] * price_scale[i-1] * precisions[i], PRECISION) + xp_old[i] = unsafe_div( + xp_old[i] * unsafe_mul(price_scale[i-1], precisions[i]), + PRECISION + ) + + # ---------------- transferFrom token into the pool ---------------------- + + for i in range(N_COINS): + + if amounts[i] > 0: + + if coins[i] == WETH20: + + self._transfer_in( + coins[i], + amounts[i], + 0, # <----------------------------------- + msg.value, # | No callbacks + empty(address), # <----------------------| for + empty(bytes32), # <----------------------| add_liquidity. + msg.sender, # | + empty(address), # <----------------------- + use_eth + ) + + else: + + self._transfer_in( + coins[i], + amounts[i], + 0, + 0, # <----------------- mvalue = 0 if coin is not WETH20. + empty(address), + empty(bytes32), + msg.sender, + empty(address), + False # <-------- use_eth is False if coin is not WETH20. + ) + + amountsp[i] = xp[i] - xp_old[i] + + # -------------------- Calculate LP tokens to mint ----------------------- + + if self.future_A_gamma_time > block.timestamp: # <--- A_gamma is ramping. + + # ----- Recalculate the invariant if A or gamma are undergoing a ramp. + old_D = MATH.newton_D(A_gamma[0], A_gamma[1], xp_old, 0) + + else: + + old_D = self.D + + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + + token_supply: uint256 = self.totalSupply + if old_D > 0: + d_token = token_supply * D / old_D - token_supply + else: + d_token = self.get_xcp(D) # <------------------------- Making initial + # virtual price equal to 1. + + assert d_token > 0 # dev: nothing minted + + if old_D > 0: + + d_token_fee = ( + self._calc_token_fee(amountsp, xp) * d_token / 10**10 + 1 + ) + + d_token -= d_token_fee + token_supply += d_token + self.mint(receiver, d_token) + + packed_price_scale = self.tweak_price(A_gamma, xp, D, 0) + + else: + + self.D = D + self.virtual_price = 10**18 + self.xcp_profit = 10**18 + self.xcp_profit_a = 10**18 + self.mint(receiver, d_token) + + assert d_token >= min_mint_amount, "Slippage" + + log AddLiquidity( + receiver, amounts, d_token_fee, token_supply, packed_price_scale + ) + + self._claim_admin_fees() # <--------------------------- Claim admin fees. + + return d_token + + +@external +@nonreentrant("lock") +def remove_liquidity( + _amount: uint256, + min_amounts: uint256[N_COINS], + use_eth: bool = False, + receiver: address = msg.sender, + claim_admin_fees: bool = True, +) -> uint256[N_COINS]: + """ + @notice This withdrawal method is very safe, does no complex math since + tokens are withdrawn in balanced proportions. No fees are charged. + @param _amount Amount of LP tokens to burn + @param min_amounts Minimum amounts of tokens to withdraw + @param use_eth Whether to withdraw ETH or not + @param receiver Address to send the withdrawn tokens to + @param claim_admin_fees If True, call self._claim_admin_fees(). Default is True. + @return uint256[3] Amount of pool tokens received by the `receiver` + """ + amount: uint256 = _amount + balances: uint256[N_COINS] = self.balances + d_balances: uint256[N_COINS] = empty(uint256[N_COINS]) + + if claim_admin_fees: + self._claim_admin_fees() # <------ We claim fees so that the DAO gets + # paid before withdrawal. In emergency cases, set it to False. + + # -------------------------------------------------------- Burn LP tokens. + + total_supply: uint256 = self.totalSupply # <------ Get totalSupply before + self.burnFrom(msg.sender, _amount) # ---- reducing it with self.burnFrom. + + # There are two cases for withdrawing tokens from the pool. + # Case 1. Withdrawal does not empty the pool. + # In this situation, D is adjusted proportional to the amount of + # LP tokens burnt. ERC20 tokens transferred is proportional + # to : (AMM balance * LP tokens in) / LP token total supply + # Case 2. Withdrawal empties the pool. + # In this situation, all tokens are withdrawn and the invariant + # is reset. + + if amount == total_supply: # <----------------------------------- Case 2. + + for i in range(N_COINS): + + d_balances[i] = balances[i] + self.balances[i] = 0 # <------------------------- Empty the pool. + + else: # <-------------------------------------------------------- Case 1. + + amount -= 1 # <---- To prevent rounding errors, favor LPs a tiny bit. + + for i in range(N_COINS): + d_balances[i] = balances[i] * amount / total_supply + assert d_balances[i] >= min_amounts[i] + self.balances[i] = balances[i] - d_balances[i] + balances[i] = d_balances[i] # <-- Now it's the amounts going out. + + D: uint256 = self.D + self.D = D - unsafe_div(D * amount, total_supply) # <----------- Reduce D + # proportional to the amount of tokens leaving. Since withdrawals are + # balanced, this is a simple subtraction. If amount == total_supply, + # D will be 0. + + # ---------------------------------- Transfers --------------------------- + + for i in range(N_COINS): + self._transfer_out(coins[i], d_balances[i], use_eth, receiver) + + log RemoveLiquidity(msg.sender, balances, total_supply - _amount) + + return d_balances + + +@external +@nonreentrant("lock") +def remove_liquidity_one_coin( + token_amount: uint256, + i: uint256, + min_amount: uint256, + use_eth: bool = False, + receiver: address = msg.sender +) -> uint256: + """ + @notice Withdraw liquidity in a single token. + Involves fees (lower than swap fees). + @dev This operation also involves an admin fee claim. + @param token_amount Amount of LP tokens to burn + @param i Index of the token to withdraw + @param min_amount Minimum amount of token to withdraw. + @param use_eth Whether to withdraw ETH or not + @param receiver Address to send the withdrawn tokens to + @return Amount of tokens at index i received by the `receiver` + """ + + A_gamma: uint256[2] = self._A_gamma() + + dy: uint256 = 0 + D: uint256 = 0 + p: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + approx_fee: uint256 = 0 + + # ---------------------------- Claim admin fees before removing liquidity. + self._claim_admin_fees() + + # ------------------------------------------------------------------------ + + dy, D, xp, approx_fee = self._calc_withdraw_one_coin( + A_gamma, + token_amount, + i, + (self.future_A_gamma_time > block.timestamp), # <------- During ramps + ) # we need to update D. + + assert dy >= min_amount, "Slippage" + + # ------------------------- Transfers ------------------------------------ + + self.balances[i] -= dy + self.burnFrom(msg.sender, token_amount) + self._transfer_out(coins[i], dy, use_eth, receiver) + + packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0) + # Safe to use D from _calc_withdraw_one_coin here ---^ + + log RemoveLiquidityOne( + msg.sender, token_amount, i, dy, approx_fee, packed_price_scale + ) + + return dy + + +@external +@nonreentrant("lock") +def claim_admin_fees(): + """ + @notice Claim admin fees. Callable by anyone. + """ + self._claim_admin_fees() + + +# -------------------------- Packing functions ------------------------------- + + +@internal +@view +def _pack(x: uint256[3]) -> uint256: + """ + @notice Packs 3 integers with values <= 10**18 into a uint256 + @param x The uint256[3] to pack + @return uint256 Integer with packed values + """ + return (x[0] << 128) | (x[1] << 64) | x[2] + + +@internal +@view +def _unpack(_packed: uint256) -> uint256[3]: + """ + @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) + @param val The uint256 to unpack + @return uint256[3] A list of length 3 with unpacked integers + """ + return [ + (_packed >> 128) & 18446744073709551615, + (_packed >> 64) & 18446744073709551615, + _packed & 18446744073709551615, + ] + + +@internal +@view +def _pack_prices(prices_to_pack: uint256[N_COINS-1]) -> uint256: + """ + @notice Packs N_COINS-1 prices into a uint256. + @param prices_to_pack The prices to pack + @return uint256 An integer that packs prices + """ + packed_prices: uint256 = 0 + p: uint256 = 0 + for k in range(N_COINS - 1): + packed_prices = packed_prices << PRICE_SIZE + p = prices_to_pack[N_COINS - 2 - k] + assert p < PRICE_MASK + packed_prices = p | packed_prices + return packed_prices + + +@internal +@view +def _unpack_prices(_packed_prices: uint256) -> uint256[2]: + """ + @notice Unpacks N_COINS-1 prices from a uint256. + @param _packed_prices The packed prices + @return uint256[2] Unpacked prices + """ + unpacked_prices: uint256[N_COINS-1] = empty(uint256[N_COINS-1]) + packed_prices: uint256 = _packed_prices + for k in range(N_COINS - 1): + unpacked_prices[k] = packed_prices & PRICE_MASK + packed_prices = packed_prices >> PRICE_SIZE + + return unpacked_prices + + +# ---------------------- AMM Internal Functions ------------------------------- + + +@internal +def _exchange( + sender: address, + mvalue: uint256, + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + use_eth: bool, + receiver: address, + callbacker: address, + callback_sig: bytes32 +) -> uint256: + + assert i != j # dev: coin index out of range + assert dx > 0 # dev: do not exchange 0 coins + + A_gamma: uint256[2] = self._A_gamma() + xp: uint256[N_COINS] = self.balances + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + dy: uint256 = 0 + + y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert. + x0: uint256 = xp[i] # <--------------- if i > N_COINS, this will revert. + xp[i] = x0 + dx + self.balances[i] = xp[i] + + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS - 1] = self._unpack_prices( + packed_price_scale + ) + + xp[0] *= precisions[0] + for k in range(1, N_COINS): + xp[k] = unsafe_div( + xp[k] * price_scale[k - 1] * precisions[k], + PRECISION + ) # <-------- Safu to do unsafe_div here since PRECISION is not zero. + + prec_i: uint256 = precisions[i] + + # ----------- Update invariant if A, gamma are undergoing ramps --------- + + t: uint256 = self.future_A_gamma_time + if t > block.timestamp: + + x0 *= prec_i + + if i > 0: + x0 = unsafe_div(x0 * price_scale[i - 1], PRECISION) + + x1: uint256 = xp[i] # <------------------ Back up old value in xp ... + xp[i] = x0 # | + self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # | + xp[i] = x1 # <-------------------------------------- ... and restore. + + # ----------------------- Calculate dy and fees -------------------------- + + D: uint256 = self.D + prec_j: uint256 = precisions[j] + y_out: uint256[2] = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, j) + dy = xp[j] - y_out[0] + xp[j] -= dy + dy -= 1 + + if j > 0: + dy = dy * PRECISION / price_scale[j - 1] + dy /= prec_j + + fee: uint256 = unsafe_div(self._fee(xp) * dy, 10**10) + + dy -= fee # <--------------------- Subtract fee from the outgoing amount. + assert dy >= min_dy, "Slippage" + + y -= dy + self.balances[j] = y # <----------- Update pool balance of outgoing coin. + + y *= prec_j + if j > 0: + y = unsafe_div(y * price_scale[j - 1], PRECISION) + xp[j] = y # <------------------------------------------------- Update xp. + + # ---------------------- Do Transfers in and out ------------------------- + + ########################## TRANSFER IN <------- + self._transfer_in( + coins[i], dx, dy, mvalue, + callbacker, callback_sig, # <-------- Callback method is called here. + sender, receiver, use_eth, + ) + + ########################## -------> TRANSFER OUT + self._transfer_out(coins[j], dy, use_eth, receiver) + + # ------ Tweak price_scale with good initial guess for newton_D ---------- + + packed_price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1]) + + log TokenExchange(sender, i, dx, j, dy, fee, packed_price_scale) + + return dy + + +@internal +def tweak_price( + A_gamma: uint256[2], + _xp: uint256[N_COINS], + new_D: uint256, + K0_prev: uint256 = 0, +) -> uint256: + """ + @notice Tweaks price_oracle, last_price and conditionally adjusts + price_scale. This is called whenever there is an unbalanced + liquidity operation: _exchange, add_liquidity, or + remove_liquidity_one_coin. + @dev Contains main liquidity rebalancing logic, by tweaking `price_scale`. + @param A_gamma Array of A and gamma parameters. + @param _xp Array of current balances. + @param new_D New D value. + @param K0_prev Initial guess for `newton_D`. + """ + + # ---------------------------- Read storage ------------------------------ + + rebalancing_params: uint256[3] = self._unpack( + self.packed_rebalancing_params + ) # <---------- Contains: allowed_extra_profit, adjustment_step, ma_time. + price_oracle: uint256[N_COINS - 1] = self._unpack_prices( + self.price_oracle_packed + ) + last_prices: uint256[N_COINS - 1] = self._unpack_prices( + self.last_prices_packed + ) + packed_price_scale: uint256 = self.price_scale_packed + price_scale: uint256[N_COINS - 1] = self._unpack_prices( + packed_price_scale + ) + + total_supply: uint256 = self.totalSupply + old_xcp_profit: uint256 = self.xcp_profit + old_virtual_price: uint256 = self.virtual_price + last_prices_timestamp: uint256 = self.last_prices_timestamp + + # ----------------------- Update MA if needed ---------------------------- + + if last_prices_timestamp < block.timestamp: + + # The moving average price oracle is calculated using the last_price + # of the trade at the previous block, and the price oracle logged + # before that trade. This can happen only once per block. + + # ------------------ Calculate moving average params ----------------- + + alpha: uint256 = MATH.wad_exp( + -convert( + unsafe_div( + (block.timestamp - last_prices_timestamp) * 10**18, + rebalancing_params[2] # <----------------------- ma_time. + ), + int256, + ) + ) + + for k in range(N_COINS - 1): + + # ----------------- We cap state price that goes into the EMA with + # 2 x price_scale. + price_oracle[k] = unsafe_div( + min(last_prices[k], 2 * price_scale[k]) * (10**18 - alpha) + + price_oracle[k] * alpha, # ^-------- Cap spot price into EMA. + 10**18 + ) + + self.price_oracle_packed = self._pack_prices(price_oracle) + self.last_prices_timestamp = block.timestamp # <---- Store timestamp. + + # price_oracle is used further on to calculate its vector + # distance from price_scale. This distance is used to calculate + # the amount of adjustment to be done to the price_scale. + + # ------------------ If new_D is set to 0, calculate it ------------------ + + D_unadjusted: uint256 = new_D + if new_D == 0: # <--------------------------- _exchange sets new_D to 0. + D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev) + + # ----------------------- Calculate last_prices -------------------------- + + last_prices = MATH.get_p(_xp, D_unadjusted, A_gamma) + for k in range(N_COINS - 1): + last_prices[k] = unsafe_div(last_prices[k] * price_scale[k], 10**18) + self.last_prices_packed = self._pack_prices(last_prices) + + # ---------- Update profit numbers without price adjustment first -------- + + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + xp[0] = unsafe_div(D_unadjusted, N_COINS) + for k in range(N_COINS - 1): + xp[k + 1] = D_unadjusted * 10**18 / (N_COINS * price_scale[k]) + + # ------------------------- Update xcp_profit ---------------------------- + + xcp_profit: uint256 = 10**18 + virtual_price: uint256 = 10**18 + + if old_virtual_price > 0: + + xcp: uint256 = MATH.geometric_mean(xp) + virtual_price = 10**18 * xcp / total_supply + + xcp_profit = unsafe_div( + old_xcp_profit * virtual_price, + old_virtual_price + ) # <---------------- Safu to do unsafe_div as old_virtual_price > 0. + + # If A and gamma are not undergoing ramps (t < block.timestamp), + # ensure new virtual_price is not less than old virtual_price, + # else the pool suffers a loss. + if self.future_A_gamma_time < block.timestamp: + assert virtual_price > old_virtual_price, "Loss" + + self.xcp_profit = xcp_profit + + # ------------ Rebalance liquidity if there's enough profits to adjust it: + if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]: + # allowed_extra_profit --------^ + + # ------------------- Get adjustment step ---------------------------- + + # Calculate the vector distance between price_scale and + # price_oracle. + norm: uint256 = 0 + ratio: uint256 = 0 + for k in range(N_COINS - 1): + + ratio = unsafe_div(price_oracle[k] * 10**18, price_scale[k]) + # unsafe_div because we did safediv before ----^ + + if ratio > 10**18: + ratio = unsafe_sub(ratio, 10**18) + else: + ratio = unsafe_sub(10**18, ratio) + norm = unsafe_add(norm, ratio**2) + + norm = isqrt(norm) # <-------------------- isqrt is not in base 1e18. + adjustment_step: uint256 = max( + rebalancing_params[1], unsafe_div(norm, 5) + ) # ^------------------------------------- adjustment_step. + + if norm > adjustment_step: # <---------- We only adjust prices if the + # vector distance between price_oracle and price_scale is + # large enough. This check ensures that no rebalancing + # occurs if the distance is low i.e. the pool prices are + # pegged to the oracle prices. + + # ------------------------------------- Calculate new price scale. + + p_new: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1]) + for k in range(N_COINS - 1): + p_new[k] = unsafe_div( + price_scale[k] * unsafe_sub(norm, adjustment_step) + + adjustment_step * price_oracle[k], + norm + ) # <- norm is non-zero and gt adjustment_step; unsafe = safe + + # ---------------- Update stale xp (using price_scale) with p_new. + xp = _xp + for k in range(N_COINS - 1): + xp[k + 1] = unsafe_div(_xp[k + 1] * p_new[k], price_scale[k]) + # unsafe_div because we did safediv before ----^ + + # ------------------------------------------ Update D with new xp. + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + + for k in range(N_COINS): + frac: uint256 = xp[k] * 10**18 / D # <----- Check validity of + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # p_new. + + xp[0] = D / N_COINS + for k in range(N_COINS - 1): + xp[k + 1] = D * 10**18 / (N_COINS * p_new[k]) # <---- Convert + # xp to real prices. + + # ---------- Calculate new virtual_price using new xp and D. Reuse + # `old_virtual_price` (but it has new virtual_price). + old_virtual_price = unsafe_div( + 10**18 * MATH.geometric_mean(xp), total_supply + ) # <----- unsafe_div because we did safediv before (if vp>1e18) + + # ---------------------------- Proceed if we've got enough profit. + if ( + old_virtual_price > 10**18 and + 2 * old_virtual_price - 10**18 > xcp_profit + ): + + packed_price_scale = self._pack_prices(p_new) + + self.D = D + self.virtual_price = old_virtual_price + self.price_scale_packed = packed_price_scale + + return packed_price_scale + + # --------- price_scale was not adjusted. Update the profit counter and D. + self.D = D_unadjusted + self.virtual_price = virtual_price + + return packed_price_scale + + +@internal +def _claim_admin_fees(): + """ + @notice Claims admin fees and sends it to fee_receiver set in the factory. + """ + A_gamma: uint256[2] = self._A_gamma() + + xcp_profit: uint256 = self.xcp_profit # <---------- Current pool profits. + xcp_profit_a: uint256 = self.xcp_profit_a # <- Profits at previous claim. + total_supply: uint256 = self.totalSupply + + # Do not claim admin fees if: + # 1. insufficient profits accrued since last claim, and + # 2. there are less than 10**18 (or 1 unit of) lp tokens, else it can lead + # to manipulated virtual prices. + if xcp_profit <= xcp_profit_a or total_supply < 10**18: + return + + # Claim tokens belonging to the admin here. This is done by 'gulping' + # pool tokens that have accrued as fees, but not accounted in pool's + # `self.balances` yet: pool balances only account for incoming and + # outgoing tokens excluding fees. Following 'gulps' fees: + + for i in range(N_COINS): + if coins[i] == WETH20: + self.balances[i] = self.balance + else: + self.balances[i] = ERC20(coins[i]).balanceOf(self) + + # If the pool has made no profits, `xcp_profit == xcp_profit_a` + # and the pool gulps nothing in the previous step. + + vprice: uint256 = self.virtual_price + + # Admin fees are calculated as follows. + # 1. Calculate accrued profit since last claim. `xcp_profit` + # is the current profits. `xcp_profit_a` is the profits + # at the previous claim. + # 2. Take out admin's share, which is hardcoded at 5 * 10**9. + # (50% => half of 100% => 10**10 / 2 => 5 * 10**9). + # 3. Since half of the profits go to rebalancing the pool, we + # are left with half; so divide by 2. + + fees: uint256 = unsafe_div( + unsafe_sub(xcp_profit, xcp_profit_a) * ADMIN_FEE, 2 * 10**10 + ) + + # ------------------------------ Claim admin fees by minting admin's share + # of the pool in LP tokens. + receiver: address = Factory(self.factory).fee_receiver() + if receiver != empty(address) and fees > 0: + + frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18 + claimed: uint256 = self.mint_relative(receiver, frac) + + xcp_profit -= fees * 2 + + self.xcp_profit = xcp_profit + + log ClaimAdminFee(receiver, claimed) + + # ------------------------------------------- Recalculate D b/c we gulped. + D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], self.xp(), 0) + self.D = D + + # ------------------- Recalculate virtual_price following admin fee claim. + # In this instance we do not check if current virtual price is greater + # than old virtual price, since the claim process can result + # in a small decrease in pool's value. + + self.virtual_price = 10**18 * self.get_xcp(D) / self.totalSupply + self.xcp_profit_a = xcp_profit # <------------ Cache last claimed profit. + + +@internal +@view +def xp() -> uint256[N_COINS]: + + result: uint256[N_COINS] = self.balances + packed_prices: uint256 = self.price_scale_packed + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + + result[0] *= precisions[0] + for i in range(1, N_COINS): + p: uint256 = (packed_prices & PRICE_MASK) * precisions[i] + result[i] = result[i] * p / PRECISION + packed_prices = packed_prices >> PRICE_SIZE + + return result + + +@view +@internal +def _A_gamma() -> uint256[2]: + t1: uint256 = self.future_A_gamma_time + + A_gamma_1: uint256 = self.future_A_gamma + gamma1: uint256 = A_gamma_1 & 2**128 - 1 + A1: uint256 = A_gamma_1 >> 128 + + if block.timestamp < t1: + + # --------------- Handle ramping up and down of A -------------------- + + A_gamma_0: uint256 = self.initial_A_gamma + t0: uint256 = self.initial_A_gamma_time + + t1 -= t0 + t0 = block.timestamp - t0 + t2: uint256 = t1 - t0 + + A1 = ((A_gamma_0 >> 128) * t2 + A1 * t0) / t1 + gamma1 = ((A_gamma_0 & 2**128 - 1) * t2 + gamma1 * t0) / t1 + + return [A1, gamma1] + + +@internal +@view +def _fee(xp: uint256[N_COINS]) -> uint256: + fee_params: uint256[3] = self._unpack(self.packed_fee_params) + f: uint256 = MATH.reduction_coefficient(xp, fee_params[2]) + return unsafe_div( + fee_params[0] * f + fee_params[1] * (10**18 - f), + 10**18 + ) + + +@internal +@view +def get_xcp(D: uint256) -> uint256: + + x: uint256[N_COINS] = empty(uint256[N_COINS]) + x[0] = D / N_COINS + packed_prices: uint256 = self.price_scale_packed # <-- No precisions here + # because we don't switch to "real" units. + + for i in range(1, N_COINS): + x[i] = D * 10**18 / (N_COINS * (packed_prices & PRICE_MASK)) + packed_prices = packed_prices >> PRICE_SIZE + + return MATH.geometric_mean(x) + + +@view +@internal +def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256: + # fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts) + fee: uint256 = unsafe_div( + unsafe_mul(self._fee(xp), N_COINS), + unsafe_mul(4, unsafe_sub(N_COINS, 1)) + ) + + S: uint256 = 0 + for _x in amounts: + S += _x + + avg: uint256 = unsafe_div(S, N_COINS) + Sdiff: uint256 = 0 + + for _x in amounts: + if _x > avg: + Sdiff += unsafe_sub(_x, avg) + else: + Sdiff += unsafe_sub(avg, _x) + + return fee * Sdiff / S + NOISE_FEE + + +@internal +@view +def _calc_withdraw_one_coin( + A_gamma: uint256[2], + token_amount: uint256, + i: uint256, + update_D: bool, +) -> (uint256, uint256, uint256[N_COINS], uint256): + + token_supply: uint256 = self.totalSupply + assert token_amount <= token_supply # dev: token amount more than supply + assert i < N_COINS # dev: coin out of range + + xx: uint256[N_COINS] = self.balances + precisions: uint256[N_COINS] = self._unpack(self.packed_precisions) + xp: uint256[N_COINS] = precisions + D0: uint256 = 0 + + # -------------------------- Calculate D0 and xp ------------------------- + + price_scale_i: uint256 = PRECISION * precisions[0] + packed_prices: uint256 = self.price_scale_packed + xp[0] *= xx[0] + for k in range(1, N_COINS): + p: uint256 = (packed_prices & PRICE_MASK) + if i == k: + price_scale_i = p * xp[i] + xp[k] = unsafe_div(xp[k] * xx[k] * p, PRECISION) + packed_prices = packed_prices >> PRICE_SIZE + + if update_D: # <-------------- D is updated if pool is undergoing a ramp. + D0 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) + else: + D0 = self.D + + D: uint256 = D0 + + # -------------------------------- Fee Calc ------------------------------ + + # Charge fees on D. Roughly calculate xp[i] after withdrawal and use that + # to calculate fee. Precision is not paramount here: we just want a + # behavior where the higher the imbalance caused the more fee the AMM + # charges. + + # xp is adjusted assuming xp[0] ~= xp[1] ~= x[2], which is usually not the + # case. We charge self._fee(xp), where xp is an imprecise adjustment post + # withdrawal in one coin. If the withdraw is too large: charge max fee by + # default. This is because the fee calculation will otherwise underflow. + + xp_imprecise: uint256[N_COINS] = xp + xp_correction: uint256 = xp[i] * N_COINS * token_amount / token_supply + fee: uint256 = self._unpack(self.packed_fee_params)[1] # <- self.out_fee. + + if xp_correction < xp_imprecise[i]: + xp_imprecise[i] -= xp_correction + fee = self._fee(xp_imprecise) + + dD: uint256 = unsafe_div(token_amount * D, token_supply) + D_fee: uint256 = fee * dD / (2 * 10**10) + 1 # <------- Actual fee on D. + + # --------- Calculate `approx_fee` (assuming balanced state) in ith token. + # -------------------------------- We only need this for fee in the event. + approx_fee: uint256 = N_COINS * D_fee * xx[i] / D + + # ------------------------------------------------------------------------ + D -= (dD - D_fee) # <----------------------------------- Charge fee on D. + # --------------------------------- Calculate `y_out`` with `(D - D_fee)`. + y: uint256 = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, i)[0] + dy: uint256 = (xp[i] - y) * PRECISION / price_scale_i + xp[i] = y + + return dy, D, xp, approx_fee + + +# ------------------------ ERC20 functions ----------------------------------- + + +@internal +def _approve(_owner: address, _spender: address, _value: uint256): + self.allowance[_owner][_spender] = _value + + log Approval(_owner, _spender, _value) + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + assert _to not in [self, empty(address)] + + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + + log Transfer(_from, _to, _value) + + +@view +@internal +def _domain_separator() -> bytes32: + if chain.id != CACHED_CHAIN_ID: + return keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + return CACHED_DOMAIN_SEPARATOR + + +@external +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + """ + @dev Transfer tokens from one address to another. + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + @return bool True on successul transfer. Reverts otherwise. + """ + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != max_value(uint256): + self._approve(_from, msg.sender, _allowance - _value) + + self._transfer(_from, _to, _value) + return True + + +@external +def transfer(_to: address, _value: uint256) -> bool: + """ + @dev Transfer token for a specified address + @param _to The address to transfer to. + @param _value The amount to be transferred. + @return bool True on successful transfer. Reverts otherwise. + """ + self._transfer(msg.sender, _to, _value) + return True + + +@external +def approve(_spender: address, _value: uint256) -> bool: + """ + @notice Allow `_spender` to transfer up to `_value` amount + of tokens from the caller's account. + @dev Non-zero to non-zero approvals are allowed, but should + be used cautiously. The methods increaseAllowance + decreaseAllowance + are available to prevent any front-running that may occur. + @param _spender The account permitted to spend up to `_value` amount of + caller's funds. + @param _value The amount of tokens `_spender` is allowed to spend. + @return bool Success + """ + self._approve(msg.sender, _spender, _value) + return True + + +@external +def increaseAllowance(_spender: address, _add_value: uint256) -> bool: + """ + @notice Increase the allowance granted to `_spender`. + @dev This function will never overflow, and instead will bound + allowance to max_value(uint256). This has the potential to grant an + infinite approval. + @param _spender The account to increase the allowance of. + @param _add_value The amount to increase the allowance by. + @return bool Success + """ + cached_allowance: uint256 = self.allowance[msg.sender][_spender] + allowance: uint256 = unsafe_add(cached_allowance, _add_value) + + if allowance < cached_allowance: # <-------------- Check for an overflow. + allowance = max_value(uint256) + + if allowance != cached_allowance: + self._approve(msg.sender, _spender, allowance) + + return True + + +@external +def decreaseAllowance(_spender: address, _sub_value: uint256) -> bool: + """ + @notice Decrease the allowance granted to `_spender`. + @dev This function will never underflow, and instead will bound + allowance to 0. + @param _spender The account to decrease the allowance of. + @param _sub_value The amount to decrease the allowance by. + @return bool Success. + """ + cached_allowance: uint256 = self.allowance[msg.sender][_spender] + allowance: uint256 = unsafe_sub(cached_allowance, _sub_value) + + if cached_allowance < allowance: # <------------- Check for an underflow. + allowance = 0 + + if allowance != cached_allowance: + self._approve(msg.sender, _spender, allowance) + + return True + + +@external +def permit( + _owner: address, + _spender: address, + _value: uint256, + _deadline: uint256, + _v: uint8, + _r: bytes32, + _s: bytes32, +) -> bool: + """ + @notice Permit `_spender` to spend up to `_value` amount of `_owner`'s + tokens via a signature. + @dev In the event of a chain fork, replay attacks are prevented as + domain separator is recalculated. However, this is only if the + resulting chains update their chainId. + @param _owner The account which generated the signature and is granting an + allowance. + @param _spender The account which will be granted an allowance. + @param _value The approval amount. + @param _deadline The deadline by which the signature must be submitted. + @param _v The last byte of the ECDSA signature. + @param _r The first 32 bytes of the ECDSA signature. + @param _s The second 32 bytes of the ECDSA signature. + @return bool Success. + """ + assert _owner != empty(address) # dev: invalid owner + assert block.timestamp <= _deadline # dev: permit expired + + nonce: uint256 = self.nonces[_owner] + digest: bytes32 = keccak256( + concat( + b"\x19\x01", + self._domain_separator(), + keccak256( + _abi_encode( + EIP2612_TYPEHASH, _owner, _spender, _value, nonce, _deadline + ) + ), + ) + ) + assert ecrecover(digest, _v, _r, _s) == _owner # dev: invalid signature + + self.nonces[_owner] = unsafe_add(nonce, 1) # <-- Unsafe add is safe here. + self._approve(_owner, _spender, _value) + return True + + +@internal +def mint(_to: address, _value: uint256) -> bool: + """ + @dev Mint an amount of the token and assigns it to an account. + This encapsulates the modification of balances such that the + proper events are emitted. + @param _to The account that will receive the created tokens. + @param _value The amount that will be created. + @return bool Success. + """ + self.totalSupply += _value + self.balanceOf[_to] += _value + + log Transfer(empty(address), _to, _value) + return True + + +@internal +def mint_relative(_to: address, frac: uint256) -> uint256: + """ + @dev Increases supply by factor of (1 + frac/1e18) and mints it for _to + @param _to The account that will receive the created tokens. + @param frac The fraction of the current supply to mint. + @return uint256 Amount of tokens minted. + """ + supply: uint256 = self.totalSupply + d_supply: uint256 = supply * frac / 10**18 + if d_supply > 0: + self.totalSupply = supply + d_supply + self.balanceOf[_to] += d_supply + log Transfer(empty(address), _to, d_supply) + + return d_supply + + +@internal +def burnFrom(_to: address, _value: uint256) -> bool: + """ + @dev Burn an amount of the token from a given account. + @param _to The account whose tokens will be burned. + @param _value The amount that will be burned. + @return bool Success. + """ + self.totalSupply -= _value + self.balanceOf[_to] -= _value + + log Transfer(_to, empty(address), _value) + return True + + +# ------------------------- AMM View Functions ------------------------------- + + +@external +@view +def fee_receiver() -> address: + """ + @notice Returns the address of the admin fee receiver. + @return address Fee receiver. + """ + return Factory(self.factory).fee_receiver() + + +@external +@view +def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256: + """ + @notice Calculate LP tokens minted or to be burned for depositing or + removing `amounts` of coins + @dev Includes fee. + @param amounts Amounts of tokens being deposited or withdrawn + @param deposit True if it is a deposit action, False if withdrawn. + @return uint256 Amount of LP tokens deposited or withdrawn. + """ + view_contract: address = Factory(self.factory).views_implementation() + return Views(view_contract).calc_token_amount(amounts, deposit, self) + + +@external +@view +def get_dy(i: uint256, j: uint256, dx: uint256) -> uint256: + """ + @notice Get amount of coin[j] tokens received for swapping in dx amount of coin[i] + @dev Includes fee. + @param i index of input token. Check pool.coins(i) to get coin address at ith index + @param j index of output token + @param dx amount of input coin[i] tokens + @return uint256 Exact amount of output j tokens for dx amount of i input tokens. + """ + view_contract: address = Factory(self.factory).views_implementation() + return Views(view_contract).get_dy(i, j, dx, self) + + +@external +@view +def get_dx(i: uint256, j: uint256, dy: uint256) -> uint256: + """ + @notice Get amount of coin[i] tokens to input for swapping out dy amount + of coin[j] + @dev This is an approximate method, and returns estimates close to the input + amount. Expensive to call on-chain. + @param i index of input token. Check pool.coins(i) to get coin address at + ith index + @param j index of output token + @param dy amount of input coin[j] tokens received + @return uint256 Approximate amount of input i tokens to get dy amount of j tokens. + """ + view_contract: address = Factory(self.factory).views_implementation() + return Views(view_contract).get_dx(i, j, dy, self) + + +@external +@view +@nonreentrant("lock") +def lp_price() -> uint256: + """ + @notice Calculates the current price of the LP token w.r.t coin at the + 0th index + @return uint256 LP price. + """ + + price_oracle: uint256[N_COINS-1] = self._unpack_prices( + self.price_oracle_packed + ) + return ( + 3 * self.virtual_price * MATH.cbrt(price_oracle[0] * price_oracle[1]) + ) / 10**24 + + +@external +@view +@nonreentrant("lock") +def get_virtual_price() -> uint256: + """ + @notice Calculates the current virtual price of the pool LP token. + @dev Not to be confused with `self.virtual_price` which is a cached + virtual price. + @return uint256 Virtual Price. + """ + return 10**18 * self.get_xcp(self.D) / self.totalSupply + + +@external +@view +@nonreentrant("lock") +def price_oracle(k: uint256) -> uint256: + """ + @notice Returns the oracle price of the coin at index `k` w.r.t the coin + at index 0. + @dev The oracle is an exponential moving average, with a periodicity + determined by `self.ma_time`. The aggregated prices are cached state + prices (dy/dx) calculated AFTER the latest trade. + @param k The index of the coin. + @return uint256 Price oracle value of kth coin. + """ + price_oracle: uint256 = self._unpack_prices(self.price_oracle_packed)[k] + price_scale: uint256 = self._unpack_prices(self.price_scale_packed)[k] + last_prices_timestamp: uint256 = self.last_prices_timestamp + + if last_prices_timestamp < block.timestamp: # <------------ Update moving + # average if needed. + + last_prices: uint256 = self._unpack_prices(self.last_prices_packed)[k] + ma_time: uint256 = self._unpack(self.packed_rebalancing_params)[2] + alpha: uint256 = MATH.wad_exp( + -convert( + (block.timestamp - last_prices_timestamp) * 10**18 / ma_time, + int256, + ) + ) + + # ---- We cap state price that goes into the EMA with 2 x price_scale. + return ( + min(last_prices, 2 * price_scale) * (10**18 - alpha) + + price_oracle * alpha + ) / 10**18 + + return price_oracle + + +@external +@view +def last_prices(k: uint256) -> uint256: + """ + @notice Returns last price of the coin at index `k` w.r.t the coin + at index 0. + @dev last_prices returns the quote by the AMM for an infinitesimally small swap + after the last trade. It is not equivalent to the last traded price, and + is computed by taking the partial differential of `x` w.r.t `y`. The + derivative is calculated in `get_p` and then multiplied with price_scale + to give last_prices. + @param k The index of the coin. + @return uint256 Last logged price of coin. + """ + return self._unpack_prices(self.last_prices_packed)[k] + + +@external +@view +def price_scale(k: uint256) -> uint256: + """ + @notice Returns the price scale of the coin at index `k` w.r.t the coin + at index 0. + @dev Price scale determines the price band around which liquidity is + concentrated. + @param k The index of the coin. + @return uint256 Price scale of coin. + """ + return self._unpack_prices(self.price_scale_packed)[k] + + +@external +@view +def fee() -> uint256: + """ + @notice Returns the fee charged by the pool at current state. + @dev Not to be confused with the fee charged at liquidity action, since + there the fee is calculated on `xp` AFTER liquidity is added or + removed. + @return uint256 fee bps. + """ + return self._fee(self.xp()) + + +@view +@external +def calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256: + """ + @notice Calculates output tokens with fee + @param token_amount LP Token amount to burn + @param i token in which liquidity is withdrawn + @return uint256 Amount of ith tokens received for burning token_amount LP tokens. + """ + + return self._calc_withdraw_one_coin( + self._A_gamma(), + token_amount, + i, + (self.future_A_gamma_time > block.timestamp) + )[0] + + +@external +@view +def calc_token_fee( + amounts: uint256[N_COINS], xp: uint256[N_COINS] +) -> uint256: + """ + @notice Returns the fee charged on the given amounts for add_liquidity. + @param amounts The amounts of coins being added to the pool. + @param xp The current balances of the pool multiplied by coin precisions. + @return uint256 Fee charged. + """ + return self._calc_token_fee(amounts, xp) + + +@view +@external +def A() -> uint256: + """ + @notice Returns the current pool amplification parameter. + @return uint256 A param. + """ + return self._A_gamma()[0] + + +@view +@external +def gamma() -> uint256: + """ + @notice Returns the current pool gamma parameter. + @return uint256 gamma param. + """ + return self._A_gamma()[1] + + +@view +@external +def mid_fee() -> uint256: + """ + @notice Returns the current mid fee + @return uint256 mid_fee value. + """ + return self._unpack(self.packed_fee_params)[0] + + +@view +@external +def out_fee() -> uint256: + """ + @notice Returns the current out fee + @return uint256 out_fee value. + """ + return self._unpack(self.packed_fee_params)[1] + + +@view +@external +def fee_gamma() -> uint256: + """ + @notice Returns the current fee gamma + @return uint256 fee_gamma value. + """ + return self._unpack(self.packed_fee_params)[2] + + +@view +@external +def allowed_extra_profit() -> uint256: + """ + @notice Returns the current allowed extra profit + @return uint256 allowed_extra_profit value. + """ + return self._unpack(self.packed_rebalancing_params)[0] + + +@view +@external +def adjustment_step() -> uint256: + """ + @notice Returns the current adjustment step + @return uint256 adjustment_step value. + """ + return self._unpack(self.packed_rebalancing_params)[1] + + +@view +@external +def ma_time() -> uint256: + """ + @notice Returns the current moving average time in seconds + @dev To get time in seconds, the parameter is multipled by ln(2) + One can expect off-by-one errors here. + @return uint256 ma_time value. + """ + return self._unpack(self.packed_rebalancing_params)[2] * 694 / 1000 + + +@view +@external +def precisions() -> uint256[N_COINS]: # <-------------- For by view contract. + """ + @notice Returns the precisions of each coin in the pool. + @return uint256[3] precisions of coins. + """ + return self._unpack(self.packed_precisions) + + +@external +@view +def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract. + """ + @notice Returns the fee charged by the pool at current state. + @param xp The current balances of the pool multiplied by coin precisions. + @return uint256 Fee value. + """ + return self._fee(xp) + + +@view +@external +def DOMAIN_SEPARATOR() -> bytes32: + """ + @notice EIP712 domain separator. + @return bytes32 Domain Separator set for the current chain. + """ + return self._domain_separator() + + +# ------------------------- AMM Admin Functions ------------------------------ + + +@external +def ramp_A_gamma( + future_A: uint256, future_gamma: uint256, future_time: uint256 +): + """ + @notice Initialise Ramping A and gamma parameter values linearly. + @dev Only accessible by factory admin, and only + @param future_A The future A value. + @param future_gamma The future gamma value. + @param future_time The timestamp at which the ramping will end. + """ + assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing + assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time + + A_gamma: uint256[2] = self._A_gamma() + initial_A_gamma: uint256 = A_gamma[0] << 128 + initial_A_gamma = initial_A_gamma | A_gamma[1] + + assert future_A > MIN_A - 1 + assert future_A < MAX_A + 1 + assert future_gamma > MIN_GAMMA - 1 + assert future_gamma < MAX_GAMMA + 1 + + ratio: uint256 = 10**18 * future_A / A_gamma[0] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + ratio = 10**18 * future_gamma / A_gamma[1] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + self.initial_A_gamma = initial_A_gamma + self.initial_A_gamma_time = block.timestamp + + future_A_gamma: uint256 = future_A << 128 + future_A_gamma = future_A_gamma | future_gamma + self.future_A_gamma_time = future_time + self.future_A_gamma = future_A_gamma + + log RampAgamma( + A_gamma[0], + future_A, + A_gamma[1], + future_gamma, + block.timestamp, + future_time, + ) + + +@external +def stop_ramp_A_gamma(): + """ + @notice Stop Ramping A and gamma parameters immediately. + @dev Only accessible by factory admin. + """ + assert msg.sender == Factory(self.factory).admin() # dev: only owner + + A_gamma: uint256[2] = self._A_gamma() + current_A_gamma: uint256 = A_gamma[0] << 128 + current_A_gamma = current_A_gamma | A_gamma[1] + self.initial_A_gamma = current_A_gamma + self.future_A_gamma = current_A_gamma + self.initial_A_gamma_time = block.timestamp + self.future_A_gamma_time = block.timestamp + + # ------ Now (block.timestamp < t1) is always False, so we return saved A. + + log StopRampA(A_gamma[0], A_gamma[1], block.timestamp) + + +@external +def commit_new_parameters( + _new_mid_fee: uint256, + _new_out_fee: uint256, + _new_fee_gamma: uint256, + _new_allowed_extra_profit: uint256, + _new_adjustment_step: uint256, + _new_ma_time: uint256, +): + """ + @notice Commit new parameters. + @dev Only accessible by factory admin. + @param _new_mid_fee The new mid fee. + @param _new_out_fee The new out fee. + @param _new_fee_gamma The new fee gamma. + @param _new_allowed_extra_profit The new allowed extra profit. + @param _new_adjustment_step The new adjustment step. + @param _new_ma_time The new ma time. ma_time is time_in_seconds/ln(2). + """ + assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert self.admin_actions_deadline == 0 # dev: active action + + _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY + self.admin_actions_deadline = _deadline + + # ----------------------------- Set fee params --------------------------- + + new_mid_fee: uint256 = _new_mid_fee + new_out_fee: uint256 = _new_out_fee + new_fee_gamma: uint256 = _new_fee_gamma + + current_fee_params: uint256[3] = self._unpack(self.packed_fee_params) + + if new_out_fee < MAX_FEE + 1: + assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range + else: + new_out_fee = current_fee_params[1] + + if new_mid_fee > MAX_FEE: + new_mid_fee = current_fee_params[0] + assert new_mid_fee <= new_out_fee # dev: mid-fee is too high + + if new_fee_gamma < 10**18: + assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18] + else: + new_fee_gamma = current_fee_params[2] + + self.future_packed_fee_params = self._pack( + [new_mid_fee, new_out_fee, new_fee_gamma] + ) + + # ----------------- Set liquidity rebalancing parameters ----------------- + + new_allowed_extra_profit: uint256 = _new_allowed_extra_profit + new_adjustment_step: uint256 = _new_adjustment_step + new_ma_time: uint256 = _new_ma_time + + current_rebalancing_params: uint256[3] = self._unpack(self.packed_rebalancing_params) + + if new_allowed_extra_profit > 10**18: + new_allowed_extra_profit = current_rebalancing_params[0] + + if new_adjustment_step > 10**18: + new_adjustment_step = current_rebalancing_params[1] + + if new_ma_time < 872542: # <----- Calculated as: 7 * 24 * 60 * 60 / ln(2) + assert new_ma_time > 86 # dev: MA time should be longer than 60/ln(2) + else: + new_ma_time = current_rebalancing_params[2] + + self.future_packed_rebalancing_params = self._pack( + [new_allowed_extra_profit, new_adjustment_step, new_ma_time] + ) + + # ---------------------------------- LOG --------------------------------- + + log CommitNewParameters( + _deadline, + new_mid_fee, + new_out_fee, + new_fee_gamma, + new_allowed_extra_profit, + new_adjustment_step, + new_ma_time, + ) + + +@external +@nonreentrant("lock") +def apply_new_parameters(): + """ + @notice Apply committed parameters. + @dev Only callable after admin_actions_deadline. + """ + assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time + assert self.admin_actions_deadline != 0 # dev: no active action + + self.admin_actions_deadline = 0 + + packed_fee_params: uint256 = self.future_packed_fee_params + self.packed_fee_params = packed_fee_params + + packed_rebalancing_params: uint256 = self.future_packed_rebalancing_params + self.packed_rebalancing_params = packed_rebalancing_params + + rebalancing_params: uint256[3] = self._unpack(packed_rebalancing_params) + fee_params: uint256[3] = self._unpack(packed_fee_params) + + log NewParameters( + fee_params[0], + fee_params[1], + fee_params[2], + rebalancing_params[0], + rebalancing_params[1], + rebalancing_params[2], + ) + + +@external +def revert_new_parameters(): + """ + @notice Revert committed parameters + @dev Only accessible by factory admin. Setting admin_actions_deadline to 0 + ensures a revert in apply_new_parameters. + """ + assert msg.sender == Factory(self.factory).admin() # dev: only owner + self.admin_actions_deadline = 0 diff --git a/contracts/amms/twocryptong/CurveCryptoMathOptimized2.vy b/contracts/amms/twocryptong/CurveCryptoMathOptimized2.vy new file mode 100644 index 0000000..456a9e1 --- /dev/null +++ b/contracts/amms/twocryptong/CurveCryptoMathOptimized2.vy @@ -0,0 +1,572 @@ +# 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: constant(uint256) = 2 * 10**15 + +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) -> 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 > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # 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 + + y: uint256 = self._newton_y(ANN, gamma, x, D, i) + frac: uint256 = y * 10**18 / D + assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # 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 + + 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 > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # 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), + 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 >= 10**16 - 1) and (frac < 10**20 + 1) # 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 - 1) and (frac < 10**20 + 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/amms/twocryptong/CurveCryptoViews2Optimized.vy b/contracts/amms/twocryptong/CurveCryptoViews2Optimized.vy new file mode 100644 index 0000000..1a9feed --- /dev/null +++ b/contracts/amms/twocryptong/CurveCryptoViews2Optimized.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/amms/twocryptong/CurveTwocryptoFactory.vy b/contracts/amms/twocryptong/CurveTwocryptoFactory.vy new file mode 100644 index 0000000..6c1f754 --- /dev/null +++ b/contracts/amms/twocryptong/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/amms/twocryptong/CurveTwocryptoOptimized.vy b/contracts/amms/twocryptong/CurveTwocryptoOptimized.vy new file mode 100644 index 0000000..4c409b5 --- /dev/null +++ b/contracts/amms/twocryptong/CurveTwocryptoOptimized.vy @@ -0,0 +1,2035 @@ +# 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 + xcp_ma_time: uint256 + +event RampAgamma: + initial_A: uint256 + future_A: uint256 + initial_gamma: uint256 + future_gamma: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + current_A: uint256 + current_gamma: uint256 + time: uint256 + +event ClaimAdminFee: + admin: indexed(address) + tokens: uint256[N_COINS] + + +# ----------------------- Storage/State Variables ---------------------------- + +N_COINS: constant(uint256) = 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. +cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price. + +last_prices: public(uint256) +last_timestamp: public(uint256) # idx 0 is for prices, idx 1 is for xcp. +last_xcp: public(uint256) +xcp_ma_time: public(uint256) + +initial_A_gamma: public(uint256) +initial_A_gamma_time: public(uint256) + +future_A_gamma: public(uint256) +future_A_gamma_time: public(uint256) # <------ Time when ramping is finished. +# This value is 0 (default) when pool is first deployed, and only gets +# populated by block.timestamp + future_time in `ramp_A_gamma` when the +# ramping process is initiated. After ramping is finished +# (i.e. self.future_A_gamma_time < block.timestamp), the variable is left +# and not set to 0. + +balances: public(uint256[N_COINS]) +D: public(uint256) +xcp_profit: public(uint256) +xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees. + +virtual_price: public(uint256) # <------ Cached (fast to read) virtual price. +# The cached `virtual_price` is also used internally. + +# Params that affect how price_scale get adjusted : +packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing +# parameters allowed_extra_profit, adjustment_step, and ma_time. + +# Fee params that determine dynamic fees: +packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma. + +ADMIN_FEE: public(constant(uint256)) = 5 * 10**9 # <----- 50% of earned fees. +MIN_FEE: constant(uint256) = 5 * 10**5 # <-------------------------- 0.5 BPS. +MAX_FEE: constant(uint256) = 10 * 10**9 +NOISE_FEE: constant(uint256) = 10**5 # <---------------------------- 0.1 BPS. + +# ----------------------- Admin params --------------------------------------- + +last_admin_fee_claim_timestamp: uint256 +admin_lp_virtual_balance: uint256 + +MIN_RAMP_TIME: constant(uint256) = 86400 +MIN_ADMIN_FEE_CLAIM_INTERVAL: constant(uint256) = 86400 + +A_MULTIPLIER: constant(uint256) = 10000 +MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 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) = 5 * 10**16 + +# ----------------------- 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 = self._pack_2(block.timestamp, block.timestamp) + self.xcp_profit_a = 10**18 + self.xcp_ma_time = 62324 # <--------- 12 hours default on contract start. + + # Cache DOMAIN_SEPARATOR. If chain.id is not CACHED_CHAIN_ID, then + # DOMAIN_SEPARATOR will be re-calculated each time `permit` is called. + # Otherwise, it will always use CACHED_DOMAIN_SEPARATOR. + # see: `_domain_separator()` for its implementation. + NAME_HASH = keccak256(name) + salt = _salt + CACHED_CHAIN_ID = chain.id + CACHED_DOMAIN_SEPARATOR = keccak256( + _abi_encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + chain.id, + self, + salt, + ) + ) + + log Transfer(empty(address), self, 0) # <------- Fire empty transfer from + # 0x0 to self for indexers to catch. + + +# ------------------- Token transfers in and out of the AMM ------------------ + + +@internal +def _transfer_in( + _coin_idx: uint256, + _dx: uint256, + sender: address, + expect_optimistic_transfer: bool, +) -> uint256: + """ + @notice Transfers `_coin` from `sender` to `self` and calls `callback_sig` + if it is not empty. + @params _coin_idx uint256 Index of the coin to transfer in. + @params dx amount of `_coin` to transfer into the pool. + @params sender address to transfer `_coin` from. + @params expect_optimistic_transfer bool True if pool expects user to transfer. + This is only enabled for exchange_received. + @return The amount of tokens received. + """ + coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) + + if expect_optimistic_transfer: # Only enabled in exchange_received: + # it expects the caller of exchange_received to have sent tokens to + # the pool before calling this method. + + # If someone donates extra tokens to the contract: do not acknowledge. + # We only want to know if there are dx amount of tokens. Anything extra, + # we ignore. This is why we need to check if received_amounts (which + # accounts for coin balances of the contract) is atleast dx. + # If we checked for received_amounts == dx, an extra transfer without a + # call to exchange_received will break the method. + dx: uint256 = coin_balance - self.balances[_coin_idx] + assert dx >= _dx # dev: user didn't give us coins + + # Adjust balances + self.balances[_coin_idx] += dx + + return dx + + # ----------------------------------------------- ERC20 transferFrom flow. + + # EXTERNAL CALL + assert ERC20(coins[_coin_idx]).transferFrom( + sender, + self, + _dx, + default_return_value=True + ) + + dx: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance + self.balances[_coin_idx] += dx + return dx + + +@internal +def _transfer_out(_coin_idx: uint256, _amount: uint256, receiver: address): + """ + @notice Transfer a single token from the pool to receiver. + @dev This function is called by `remove_liquidity` and + `remove_liquidity_one`, `_claim_admin_fees` and `_exchange` methods. + @params _coin_idx uint256 Index of the token to transfer out + @params _amount Amount of token to transfer out + @params receiver Address to send the tokens to + """ + + # Adjust balances before handling transfers: + self.balances[_coin_idx] -= _amount + + # EXTERNAL CALL + assert ERC20(coins[_coin_idx]).transfer( + receiver, + _amount, + default_return_value=True + ) + + +# -------------------------- AMM Main Functions ------------------------------ + + +@external +@nonreentrant("lock") +def exchange( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + receiver: address = msg.sender +) -> uint256: + """ + @notice Exchange using wrapped native token by default + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param receiver Address to send the output coin to. Default is msg.sender + @return uint256 Amount of tokens at index j received by the `receiver + """ + # _transfer_in updates self.balances here: + dx_received: uint256 = self._transfer_in( + i, + dx, + msg.sender, + False + ) + + # No ERC20 token transfers occur here: + out: uint256[3] = self._exchange( + i, + j, + dx_received, + min_dy, + ) + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(j, out[0], receiver) + + # log: + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) + + return out[0] + + +@external +@nonreentrant('lock') +def exchange_received( + i: uint256, + j: uint256, + dx: uint256, + min_dy: uint256, + receiver: address = msg.sender, +) -> uint256: + """ + @notice Exchange: but user must transfer dx amount of coin[i] tokens to pool first. + Pool will not call transferFrom and will only check if a surplus of + coins[i] is greater than or equal to `dx`. + @dev Use-case is to reduce the number of redundant ERC20 token + transfers in zaps. Primarily for dex-aggregators/arbitrageurs/searchers. + Note for users: please transfer + exchange_received in 1 tx. + @param i Index value for the input coin + @param j Index value for the output coin + @param dx Amount of input coin being swapped in + @param min_dy Minimum amount of output coin to receive + @param receiver Address to send the output coin to + @return uint256 Amount of tokens at index j received by the `receiver` + """ + # _transfer_in updates self.balances here: + dx_received: uint256 = self._transfer_in( + i, + dx, + msg.sender, + True # <---- expect_optimistic_transfer is set to True here. + ) + + # No ERC20 token transfers occur here: + out: uint256[3] = self._exchange( + i, + j, + dx_received, + min_dy, + ) + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(j, out[0], receiver) + + # log: + log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2]) + + return out[0] + + +@external +@nonreentrant("lock") +def add_liquidity( + amounts: uint256[N_COINS], + min_mint_amount: uint256, + receiver: address = msg.sender +) -> uint256: + """ + @notice Adds liquidity into the pool. + @param amounts Amounts of each coin to add. + @param min_mint_amount Minimum amount of LP to mint. + @param receiver Address to send the LP tokens to. Default is msg.sender + @return uint256 Amount of LP tokens received by the `receiver + """ + + A_gamma: uint256[2] = self._A_gamma() + xp: uint256[N_COINS] = self.balances + amountsp: uint256[N_COINS] = empty(uint256[N_COINS]) + d_token: uint256 = 0 + d_token_fee: uint256 = 0 + old_D: uint256 = 0 + + assert amounts[0] + amounts[1] > 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 + + # Initialise xcp oracle here: + self.cached_xcp_oracle = d_token # <--- virtual_price * totalSupply / 10**18 + + self.mint(receiver, d_token) + + assert d_token >= min_mint_amount, "Slippage" + + # ---------------------------------------------- Log and claim admin fees. + + log AddLiquidity( + receiver, + amounts_received, + d_token_fee, + token_supply, + price_scale + ) + + return d_token + + +@external +@nonreentrant("lock") +def remove_liquidity( + _amount: uint256, + min_amounts: uint256[N_COINS], + receiver: address = msg.sender, +) -> uint256[N_COINS]: + """ + @notice This withdrawal method is very safe, does no complex math since + tokens are withdrawn in balanced proportions. No fees are charged. + @param _amount Amount of LP tokens to burn + @param min_amounts Minimum amounts of tokens to withdraw + @param receiver Address to send the withdrawn tokens to + @return uint256[3] Amount of pool tokens received by the `receiver` + """ + amount: uint256 = _amount + balances: uint256[N_COINS] = self.balances + withdraw_amounts: uint256[N_COINS] = empty(uint256[N_COINS]) + + # -------------------------------------------------------- Burn LP tokens. + + total_supply: uint256 = self.totalSupply # <------ Get totalSupply before + self.burnFrom(msg.sender, _amount) # ---- reducing it with self.burnFrom. + + # There are two cases for withdrawing tokens from the pool. + # Case 1. Withdrawal does not empty the pool. + # In this situation, D is adjusted proportional to the amount of + # LP tokens burnt. ERC20 tokens transferred is proportional + # to : (AMM balance * LP tokens in) / LP token total supply + # Case 2. Withdrawal empties the pool. + # In this situation, all tokens are withdrawn and the invariant + # is reset. + + if amount == total_supply: # <----------------------------------- Case 2. + + for i in range(N_COINS): + + withdraw_amounts[i] = balances[i] + + else: # <-------------------------------------------------------- Case 1. + + amount -= 1 # <---- To prevent rounding errors, favor LPs a tiny bit. + + for i in range(N_COINS): + + withdraw_amounts[i] = balances[i] * amount / total_supply + assert withdraw_amounts[i] >= min_amounts[i] + + D: uint256 = self.D + self.D = D - unsafe_div(D * amount, total_supply) # <----------- Reduce D + # proportional to the amount of tokens leaving. Since withdrawals are + # balanced, this is a simple subtraction. If amount == total_supply, + # D will be 0. + + # ---------------------------------- Transfers --------------------------- + + for i in range(N_COINS): + # _transfer_out updates self.balances here. Update to state occurs + # before external calls: + self._transfer_out(i, withdraw_amounts[i], receiver) + + log RemoveLiquidity(msg.sender, withdraw_amounts, total_supply - _amount) + + # --------------------------- Upkeep xcp oracle -------------------------- + + # Update xcp since liquidity was removed: + xp: uint256[N_COINS] = self.xp(self.balances, self.cached_price_scale) + last_xcp: uint256 = isqrt(xp[0] * xp[1]) # <----------- Cache it for now. + + last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) + if last_timestamp[1] < block.timestamp: + + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + alpha: uint256 = MATH.wad_exp( + -convert( + unsafe_div( + unsafe_sub(block.timestamp, last_timestamp[1]) * 10**18, + self.xcp_ma_time # <---------- xcp ma time has is longer. + ), + int256, + ) + ) + + self.cached_xcp_oracle = unsafe_div( + last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, + 10**18 + ) + last_timestamp[1] = block.timestamp + + # Pack and store timestamps: + self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) + + # Store last xcp + self.last_xcp = last_xcp + + return withdraw_amounts + + +@external +@nonreentrant("lock") +def remove_liquidity_one_coin( + token_amount: uint256, + i: uint256, + min_amount: uint256, + receiver: address = msg.sender +) -> uint256: + """ + @notice Withdraw liquidity in a single token. + Involves fees (lower than swap fees). + @dev This operation also involves an admin fee claim. + @param token_amount Amount of LP tokens to burn + @param i Index of the token to withdraw + @param min_amount Minimum amount of token to withdraw. + @param receiver Address to send the withdrawn tokens to + @return Amount of tokens at index i received by the `receiver` + """ + + self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally. + + A_gamma: uint256[2] = self._A_gamma() + + dy: uint256 = 0 + D: uint256 = 0 + p: uint256 = 0 + xp: uint256[N_COINS] = empty(uint256[N_COINS]) + approx_fee: uint256 = 0 + + # ------------------------------------------------------------------------ + + dy, D, xp, approx_fee = self._calc_withdraw_one_coin( + A_gamma, + token_amount, + i, + (self.future_A_gamma_time > block.timestamp), # <------- During ramps + ) # we need to update D. + + assert dy >= min_amount, "Slippage" + + # ---------------------------- State Updates ----------------------------- + + # Burn user's tokens: + self.burnFrom(msg.sender, token_amount) + + packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0) + # Safe to use D from _calc_withdraw_one_coin here ---^ + + # ------------------------- Transfers ------------------------------------ + + # _transfer_out updates self.balances here. Update to state occurs before + # external calls: + self._transfer_out(i, dy, receiver) + + log RemoveLiquidityOne( + msg.sender, token_amount, i, dy, approx_fee, packed_price_scale + ) + + return dy + + +# -------------------------- Packing functions ------------------------------- + + +@internal +@pure +def _pack_3(x: uint256[3]) -> uint256: + """ + @notice Packs 3 integers with values <= 10**18 into a uint256 + @param x The uint256[3] to pack + @return uint256 Integer with packed values + """ + return (x[0] << 128) | (x[1] << 64) | x[2] + + +@internal +@pure +def _unpack_3(_packed: uint256) -> uint256[3]: + """ + @notice Unpacks a uint256 into 3 integers (values must be <= 10**18) + @param val The uint256 to unpack + @return uint256[3] A list of length 3 with unpacked integers + """ + return [ + (_packed >> 128) & 18446744073709551615, + (_packed >> 64) & 18446744073709551615, + _packed & 18446744073709551615, + ] + + +@pure +@internal +def _pack_2(p1: uint256, p2: uint256) -> uint256: + return p1 | (p2 << 128) + + +@pure +@internal +def _unpack_2(packed: uint256) -> uint256[2]: + return [packed & (2**128 - 1), packed >> 128] + + +# ---------------------- 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 Oracles if needed ----------------------- + + last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp) + alpha: uint256 = 0 + if last_timestamp[0] < block.timestamp: # 0th index is for price_oracle. + + # The moving average price oracle is calculated using the last_price + # of the trade at the previous block, and the price oracle logged + # before that trade. This can happen only once per block. + + # ------------------ Calculate moving average params ----------------- + + alpha = MATH.wad_exp( + -convert( + unsafe_div( + unsafe_sub(block.timestamp, last_timestamp[0]) * 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 + last_timestamp[0] = block.timestamp + + # ----------------------------------------------------- Update xcp oracle. + + if last_timestamp[1] < block.timestamp: + + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + alpha = MATH.wad_exp( + -convert( + unsafe_div( + unsafe_sub(block.timestamp, last_timestamp[1]) * 10**18, + self.xcp_ma_time # <---------- xcp ma time has is longer. + ), + int256, + ) + ) + + self.cached_xcp_oracle = unsafe_div( + self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha, + 10**18 + ) + + # Pack and store timestamps: + last_timestamp[1] = block.timestamp + + self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1]) + + # `price_oracle` is used further on to calculate its vector distance from + # price_scale. This distance is used to calculate the amount of adjustment + # to be done to the price_scale. + # ------------------------------------------------------------------------ + + # ------------------ If new_D is set to 0, calculate it ------------------ + + D_unadjusted: uint256 = new_D + if new_D == 0: # <--------------------------- _exchange sets new_D to 0. + D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev) + + # ----------------------- Calculate last_prices -------------------------- + + 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: + assert virtual_price > old_virtual_price, "Loss" + + # -------------------------- Cache last_xcp -------------------------- + + self.last_xcp = xcp # geometric_mean(D * price_scale) + + self.xcp_profit = xcp_profit + + # ------------ Rebalance liquidity if there's enough profits to adjust it: + if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]: + # allowed_extra_profit --------^ + + # ------------------- Get adjustment step ---------------------------- + + # Calculate the vector distance between price_scale and + # price_oracle. + norm: uint256 = 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) + + for k in range(N_COINS): + frac: uint256 = xp[k] * 10**18 / D # <----- Check validity of + assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # p_new. + + # ------------------------------------- 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._unpack_2(self.last_timestamp)[0] + + 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 xcp_oracle() -> uint256: + """ + @notice Returns the oracle value for xcp. + @dev The oracle is an exponential moving average, with a periodicity + determined by `self.xcp_ma_time`. + `TVL` is xcp, calculated as either: + 1. virtual_price * total_supply, OR + 2. self.get_xcp(...), OR + 3. MATH.geometric_mean(xp) + @return uint256 Oracle value of xcp. + """ + + last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[1] + cached_xcp_oracle: uint256 = self.cached_xcp_oracle + + if last_prices_timestamp < block.timestamp: + + alpha: uint256 = MATH.wad_exp( + -convert( + unsafe_div( + unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18, + self.xcp_ma_time + ), + int256, + ) + ) + + return (self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha) / 10**18 + + return cached_xcp_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.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing + assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time + + A_gamma: uint256[2] = self._A_gamma() + initial_A_gamma: uint256 = A_gamma[0] << 128 + initial_A_gamma = initial_A_gamma | A_gamma[1] + + assert future_A > MIN_A - 1 + assert future_A < MAX_A + 1 + assert future_gamma > MIN_GAMMA - 1 + assert future_gamma < MAX_GAMMA + 1 + + ratio: uint256 = 10**18 * future_A / A_gamma[0] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + ratio = 10**18 * future_gamma / A_gamma[1] + assert ratio < 10**18 * MAX_A_CHANGE + 1 + assert ratio > 10**18 / MAX_A_CHANGE - 1 + + self.initial_A_gamma = initial_A_gamma + self.initial_A_gamma_time = block.timestamp + + future_A_gamma: uint256 = future_A << 128 + future_A_gamma = future_A_gamma | future_gamma + self.future_A_gamma_time = future_time + self.future_A_gamma = future_A_gamma + + log RampAgamma( + A_gamma[0], + future_A, + A_gamma[1], + future_gamma, + block.timestamp, + future_time, + ) + + +@external +def stop_ramp_A_gamma(): + """ + @notice Stop Ramping A and gamma parameters immediately. + @dev Only accessible by factory admin. + """ + assert msg.sender == factory.admin() # dev: only owner + + A_gamma: uint256[2] = self._A_gamma() + current_A_gamma: uint256 = A_gamma[0] << 128 + current_A_gamma = current_A_gamma | A_gamma[1] + self.initial_A_gamma = current_A_gamma + self.future_A_gamma = current_A_gamma + self.initial_A_gamma_time = block.timestamp + self.future_A_gamma_time = block.timestamp + + # ------ Now (block.timestamp < t1) is always False, so we return saved A. + + log StopRampA(A_gamma[0], A_gamma[1], block.timestamp) + + +@external +@nonreentrant('lock') +def apply_new_parameters( + _new_mid_fee: uint256, + _new_out_fee: uint256, + _new_fee_gamma: uint256, + _new_allowed_extra_profit: uint256, + _new_adjustment_step: uint256, + _new_ma_time: uint256, + _new_xcp_ma_time: uint256, +): + """ + @notice Commit new parameters. + @dev Only accessible by factory admin. + @param _new_mid_fee The new mid fee. + @param _new_out_fee The new out fee. + @param _new_fee_gamma The new fee gamma. + @param _new_allowed_extra_profit The new allowed extra profit. + @param _new_adjustment_step The new adjustment step. + @param _new_ma_time The new ma time. ma_time is time_in_seconds/ln(2). + @param _new_xcp_ma_time The new ma time for xcp oracle. + """ + assert msg.sender == factory.admin() # dev: only owner + + # ----------------------------- Set fee params --------------------------- + + new_mid_fee: uint256 = _new_mid_fee + new_out_fee: uint256 = _new_out_fee + new_fee_gamma: uint256 = _new_fee_gamma + + current_fee_params: uint256[3] = self._unpack_3(self.packed_fee_params) + + if new_out_fee < MAX_FEE + 1: + assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range + else: + new_out_fee = current_fee_params[1] + + if new_mid_fee > MAX_FEE: + new_mid_fee = current_fee_params[0] + assert new_mid_fee <= new_out_fee # dev: mid-fee is too high + + if new_fee_gamma < 10**18: + assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18] + else: + new_fee_gamma = current_fee_params[2] + + self.packed_fee_params = self._pack_3([new_mid_fee, new_out_fee, new_fee_gamma]) + + # ----------------- Set liquidity rebalancing parameters ----------------- + + new_allowed_extra_profit: uint256 = _new_allowed_extra_profit + new_adjustment_step: uint256 = _new_adjustment_step + new_ma_time: uint256 = _new_ma_time + + current_rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params) + + if new_allowed_extra_profit > 10**18: + new_allowed_extra_profit = current_rebalancing_params[0] + + if new_adjustment_step > 10**18: + new_adjustment_step = current_rebalancing_params[1] + + if new_ma_time < 872542: # <----- Calculated as: 7 * 24 * 60 * 60 / ln(2) + assert new_ma_time > 86 # dev: MA time should be longer than 60/ln(2) + else: + new_ma_time = current_rebalancing_params[2] + + self.packed_rebalancing_params = self._pack_3( + [new_allowed_extra_profit, new_adjustment_step, new_ma_time] + ) + + # Set xcp oracle moving average window time: + new_xcp_ma_time: uint256 = _new_xcp_ma_time + if new_xcp_ma_time < 872542: + assert new_xcp_ma_time > 86 # dev: xcp MA time should be longer than 60/ln(2) + else: + new_xcp_ma_time = self.xcp_ma_time + self.xcp_ma_time = new_xcp_ma_time + + # ---------------------------------- LOG --------------------------------- + + log NewParameters( + new_mid_fee, + new_out_fee, + new_fee_gamma, + new_allowed_extra_profit, + new_adjustment_step, + new_ma_time, + _new_xcp_ma_time, + ) diff --git a/contracts/interfaces/StableFactoryNG.json b/contracts/interfaces/StableFactoryNG.json new file mode 100644 index 0000000..f4d25f0 --- /dev/null +++ b/contracts/interfaces/StableFactoryNG.json @@ -0,0 +1,996 @@ +[ + { + "name": "BasePoolAdded", + "inputs": [ + { + "name": "base_pool", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "PlainPoolDeployed", + "inputs": [ + { + "name": "coins", + "type": "address[8]", + "indexed": false + }, + { + "name": "A", + "type": "uint256", + "indexed": false + }, + { + "name": "fee", + "type": "uint256", + "indexed": false + }, + { + "name": "deployer", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "MetaPoolDeployed", + "inputs": [ + { + "name": "coin", + "type": "address", + "indexed": false + }, + { + "name": "base_pool", + "type": "address", + "indexed": false + }, + { + "name": "A", + "type": "uint256", + "indexed": false + }, + { + "name": "fee", + "type": "uint256", + "indexed": false + }, + { + "name": "deployer", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "LiquidityGaugeDeployed", + "inputs": [ + { + "name": "pool", + "type": "address", + "indexed": false + }, + { + "name": "gauge", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "constructor", + "inputs": [ + { + "name": "_fee_receiver", + "type": "address" + }, + { + "name": "_owner", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "find_pool_for_coins", + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "find_pool_for_coins", + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "i", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_base_pool", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_n_coins", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_meta_n_coins", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_coins", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address[8]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_underlying_coins", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address[8]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_decimals", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[8]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_underlying_decimals", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[8]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_metapool_rates", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[8]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_balances", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[8]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_underlying_balances", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[8]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_A", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_fees", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_admin_balances", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[8]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_coin_indices", + "inputs": [ + { + "name": "_pool", + "type": "address" + }, + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "int128" + }, + { + "name": "", + "type": "int128" + }, + { + "name": "", + "type": "bool" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_gauge", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_implementation_address", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "is_meta", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_pool_asset_types", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint8[8]" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_plain_pool", + "inputs": [ + { + "name": "_name", + "type": "string" + }, + { + "name": "_symbol", + "type": "string" + }, + { + "name": "_coins", + "type": "address[8]" + }, + { + "name": "_A", + "type": "uint256" + }, + { + "name": "_fee", + "type": "uint256" + }, + { + "name": "_offpeg_fee_multiplier", + "type": "uint256" + }, + { + "name": "_ma_exp_time", + "type": "uint256" + }, + { + "name": "_implementation_idx", + "type": "uint256" + }, + { + "name": "_asset_types", + "type": "uint8[8]" + }, + { + "name": "_method_ids", + "type": "bytes4[8]" + }, + { + "name": "_oracles", + "type": "address[8]" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_metapool", + "inputs": [ + { + "name": "_base_pool", + "type": "address" + }, + { + "name": "_name", + "type": "string" + }, + { + "name": "_symbol", + "type": "string" + }, + { + "name": "_coin", + "type": "address" + }, + { + "name": "_A", + "type": "uint256" + }, + { + "name": "_fee", + "type": "uint256" + }, + { + "name": "_offpeg_fee_multiplier", + "type": "uint256" + }, + { + "name": "_ma_exp_time", + "type": "uint256" + }, + { + "name": "_implementation_idx", + "type": "uint256" + }, + { + "name": "_asset_type", + "type": "uint8" + }, + { + "name": "_method_id", + "type": "bytes4" + }, + { + "name": "_oracle", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + { + "name": "_pool", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "add_base_pool", + "inputs": [ + { + "name": "_base_pool", + "type": "address" + }, + { + "name": "_base_lp_token", + "type": "address" + }, + { + "name": "_asset_types", + "type": "uint8[8]" + }, + { + "name": "_n_coins", + "type": "uint256" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_pool_implementations", + "inputs": [ + { + "name": "_implementation_index", + "type": "uint256" + }, + { + "name": "_implementation", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_metapool_implementations", + "inputs": [ + { + "name": "_implementation_index", + "type": "uint256" + }, + { + "name": "_implementation", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_math_implementation", + "inputs": [ + { + "name": "_math_implementation", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_gauge_implementation", + "inputs": [ + { + "name": "_gauge_implementation", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_views_implementation", + "inputs": [ + { + "name": "_views_implementation", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "commit_transfer_ownership", + "inputs": [ + { + "name": "_addr", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "accept_transfer_ownership", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_fee_receiver", + "inputs": [ + { + "name": "_pool", + "type": "address" + }, + { + "name": "_fee_receiver", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "add_asset_type", + "inputs": [ + { + "name": "_id", + "type": "uint8" + }, + { + "name": "_name", + "type": "string" + } + ], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "admin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "future_admin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "asset_types", + "inputs": [ + { + "name": "arg0", + "type": "uint8" + } + ], + "outputs": [ + { + "name": "", + "type": "string" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "pool_list", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "pool_count", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "base_pool_list", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "base_pool_count", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "base_pool_data", + "inputs": [ + { + "name": "arg0", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "components": [ + { + "name": "lp_token", + "type": "address" + }, + { + "name": "coins", + "type": "address[8]" + }, + { + "name": "decimals", + "type": "uint256" + }, + { + "name": "n_coins", + "type": "uint256" + }, + { + "name": "asset_types", + "type": "uint8[8]" + } + ] + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "base_pool_assets", + "inputs": [ + { + "name": "arg0", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "pool_implementations", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "metapool_implementations", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "math_implementation", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "gauge_implementation", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "views_implementation", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "fee_receiver", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + } + ] diff --git a/contracts/mainnet/MetaRegistry.vy b/contracts/mainnet/MetaRegistry.vy index 8660464..af1c8cb 100644 --- a/contracts/mainnet/MetaRegistry.vy +++ b/contracts/mainnet/MetaRegistry.vy @@ -1,4 +1,4 @@ -#pragma version ^0.3.7 +#pragma version 0.3.10 """ @title Curve Meta Registry @license MIT @@ -57,8 +57,8 @@ ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 # ---- storage variables ---- # -address_provider: public(AddressProvider) -owner: public(address) +admin: public(address) +future_admin: public(address) # get registry/registry_handler by index, index starts at 0: get_registry: public(HashMap[uint256, address]) @@ -67,9 +67,8 @@ registry_length: public(uint256) # ---- constructor ---- # @external -def __init__(_address_provider: address): - self.address_provider = AddressProvider(_address_provider) - self.owner = AddressProvider(_address_provider).admin() +def __init__(): + self.admin = msg.sender # ---- internal methods ---- # @@ -133,7 +132,7 @@ def add_registry_handler(_registry_handler: address): @notice Adds a registry from the address provider entry @param _registry_handler Address of the handler contract """ - assert msg.sender == self.owner # dev: only owner + assert msg.sender == self.admin # dev: only admin self._update_single_registry(self.registry_length, _registry_handler) @@ -145,7 +144,7 @@ def update_registry_handler(_index: uint256, _registry_handler: address): @param _index The index of the registry in get_registry @param _registry_handler Address of the new handler contract """ - assert msg.sender == self.owner # dev: only owner + assert msg.sender == self.admin # dev: only admin assert _index < self.registry_length self._update_single_registry(_index, _registry_handler) @@ -559,3 +558,26 @@ def pool_list(_index: uint256) -> address: return RegistryHandler(handler).pool_list(_index - pools_skip) pools_skip += count return empty(address) + + +@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 + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + self.future_admin = empty(address) diff --git a/contracts/mainnet/registries/BasePoolRegistry.vy b/contracts/mainnet/registries/BasePoolRegistry.vy index 1f5535b..f600dda 100644 --- a/contracts/mainnet/registries/BasePoolRegistry.vy +++ b/contracts/mainnet/registries/BasePoolRegistry.vy @@ -40,7 +40,9 @@ event BasePoolRemoved: basepool: indexed(address) -ADDRESS_PROVIDER: constant(address) = 0x0000000022D53366457F9d5E68Ec105046FC4383 +admin: public(address) +future_admin: public(address) + base_pool: HashMap[address, BasePool] base_pool_list: public(address[100]) get_base_pool_for_lp_token: public(HashMap[address, address]) @@ -48,6 +50,11 @@ base_pool_count: public(uint256) last_updated: public(uint256) +@external +def __init__(): + self.admin = msg.sender + + @internal @view def _get_basepool_coins(_pool: address) -> address[MAX_COINS]: @@ -225,7 +232,7 @@ def add_base_pool(_pool: address, _lp_token: address, _n_coins: uint256, _is_leg @param _is_lending True if the base pool is a Curve Lending pool @param _is_v2 True if the base pool is a Curve CryptoSwap pool """ - assert msg.sender == AddressProvider(ADDRESS_PROVIDER).admin() # dev: admin-only function + assert msg.sender == self.admin # dev: admin-only function assert self.base_pool[_pool].lp_token == empty(address) # dev: pool exists # add pool to base_pool_list @@ -252,7 +259,7 @@ def remove_base_pool(_pool: address): @notice Remove a base pool from the registry @param _pool Address of the base pool """ - assert msg.sender == AddressProvider(ADDRESS_PROVIDER).admin() # dev: admin-only function + assert msg.sender == self.admin # dev: admin-only function assert _pool != empty(address) assert self.base_pool[_pool].lp_token != empty(address) # dev: pool doesn't exist @@ -284,3 +291,26 @@ def remove_base_pool(_pool: address): self.last_updated = block.timestamp log BasePoolRemoved(_pool) + + +@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 + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + self.future_admin = empty(address) diff --git a/contracts/mainnet/registry_handlers/CryptoFactoryHandler.vy b/contracts/mainnet/registry_handlers/CryptoFactoryHandler.vy index 8cf6d1b..cb45cf6 100644 --- a/contracts/mainnet/registry_handlers/CryptoFactoryHandler.vy +++ b/contracts/mainnet/registry_handlers/CryptoFactoryHandler.vy @@ -235,7 +235,7 @@ def _get_gauge_type(_gauge: address) -> int128: success, response = raw_call( GAUGE_CONTROLLER, concat( - method_id("gauge_type(address)"), + method_id("gauge_types(address)"), convert(_gauge, bytes32), ), max_outsize=32, diff --git a/contracts/mainnet/registry_handlers/StableFactoryHandler.vy b/contracts/mainnet/registry_handlers/StableFactoryHandler.vy index 32d1fe1..ddd5a2e 100644 --- a/contracts/mainnet/registry_handlers/StableFactoryHandler.vy +++ b/contracts/mainnet/registry_handlers/StableFactoryHandler.vy @@ -196,7 +196,7 @@ def _get_gauge_type(_gauge: address) -> int128: success, response = raw_call( GAUGE_CONTROLLER, concat( - method_id("gauge_type(address)"), + method_id("gauge_types(address)"), convert(_gauge, bytes32), ), max_outsize=32, diff --git a/contracts/mainnet/registry_handlers/ng/CurveStableSwapFactoryNGHandler.vy b/contracts/mainnet/registry_handlers/ng/CurveStableSwapFactoryNGHandler.vy new file mode 100644 index 0000000..3a001a8 --- /dev/null +++ b/contracts/mainnet/registry_handlers/ng/CurveStableSwapFactoryNGHandler.vy @@ -0,0 +1,529 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveStableswapFactoryNGHandler +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2023 - all rights reserved +@notice StableswapNGFactory handler for the Metaregistry +""" + +# ---- interfaces ---- # +interface BaseRegistry: + def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: view + def get_admin_balances(_pool: address) -> DynArray[uint256, MAX_METAREGISTRY_COINS]: view + def get_A(_pool: address) -> uint256: view + def get_balances(_pool: address) -> DynArray[uint256, MAX_METAREGISTRY_COINS]: view + def get_coins(_pool: address) -> DynArray[address, MAX_METAREGISTRY_COINS]: view + def get_coin_indices(_pool: address, _from: address, _to: address) -> (int128, int128): view + def get_decimals(_pool: address) -> DynArray[uint256, MAX_METAREGISTRY_COINS]: view + def get_fees(_pool: address) -> uint256[2]: view + def get_gauge(_pool: address) -> address: view + def get_lp_token(_pool: address) -> address: view + def get_meta_n_coins(_pool: address) -> (uint256, uint256): view + def get_n_coins(_pool: address) -> uint256: view + def get_pool_asset_type(_pool: address) -> uint256: view + def get_underlying_balances(_pool: address) -> DynArray[uint256, MAX_METAREGISTRY_COINS]: view + def get_underlying_coins(_pool: address) -> DynArray[address, MAX_METAREGISTRY_COINS]: view + def get_underlying_decimals(_pool: address) -> DynArray[uint256, MAX_METAREGISTRY_COINS]: view + def is_meta(_pool: address) -> bool: view + def pool_count() -> uint256: view + def pool_list(pool_id: uint256) -> address: view + def base_pool_data(_pool: address) -> BasePoolArray: view + def base_pool_count() -> uint256: view + def get_base_pool(_pool: address) -> address: view + def base_pool_list(i: uint256) -> address: view + +interface CurveLegacyPool: + def balances(i: int128) -> uint256: view + +interface CurvePool: + def admin_balances(i: uint256) -> uint256: view + def balances(i: uint256) -> uint256: view + def get_virtual_price() -> uint256: view + def fee() -> uint256: view + def admin_fee() -> uint256: view + def offpeg_fee_multiplier() -> uint256: view + def coins(i: uint256) -> address: view + +interface ERC20: + def balanceOf(_addr: address) -> uint256: view + def decimals() -> uint256: view + def name() -> String[64]: view + def totalSupply() -> uint256: view + +interface Gauge: + def is_killed() -> bool: view + + +struct BasePoolArray: + lp_token: address + coins: DynArray[address, MAX_METAREGISTRY_COINS] + decimals: uint256 + n_coins: uint256 + asset_types: DynArray[uint8, MAX_METAREGISTRY_COINS] + + +# ---- constants ---- # +GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB +MAX_METAREGISTRY_COINS: constant(uint256) = 8 +legacy_pool: constant(address) = 0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714 + +# ---- storage variables ---- # +base_registry: public(BaseRegistry) + + +# ---- constructor ---- # +@external +def __init__(_registry_address: address): + self.base_registry = BaseRegistry(_registry_address) + + +# ---- internal methods ---- # +@internal +@view +def _get_base_pool_lp_token(_lp_token: address) -> address: + for i in range(self.base_registry.base_pool_count(), bound=10000): + base_pool: address = self.base_registry.base_pool_list(i) + if self.base_registry.base_pool_data(base_pool).lp_token == _lp_token: + return base_pool + return empty(address) + + +@internal +@view +def _get_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + _coins: DynArray[address, MAX_METAREGISTRY_COINS] = self.base_registry.get_coins(_pool) + _padded_coins: address[MAX_METAREGISTRY_COINS] = empty(address[MAX_METAREGISTRY_COINS]) + for i in range(MAX_METAREGISTRY_COINS): + if i == len(_coins): + break + _padded_coins[i] = _coins[i] + return _padded_coins + + +@internal +@view +def _get_underlying_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + _coins: DynArray[address, MAX_METAREGISTRY_COINS] = self.base_registry.get_underlying_coins(_pool) + _padded_coins: address[MAX_METAREGISTRY_COINS] = empty(address[MAX_METAREGISTRY_COINS]) + for i in range(MAX_METAREGISTRY_COINS): + if i == len(_coins): + break + _padded_coins[i] = _coins[i] + return _padded_coins + + +@view +@internal +def _get_meta_underlying_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + base_coin_idx: uint256 = self.base_registry.get_n_coins(_pool) - 1 + base_pool: address = self.base_registry.get_base_pool(_pool) + + # base pool lp token is at index 1: + base_total_supply: uint256 = ERC20(CurvePool(_pool).coins(1)).totalSupply() + + ul_balance: uint256 = 0 + underlying_pct: uint256 = 0 + if base_total_supply > 0: + underlying_pct = CurvePool(_pool).balances(base_coin_idx) * 10**36 / base_total_supply + + underlying_balances: uint256[MAX_METAREGISTRY_COINS] = empty(uint256[MAX_METAREGISTRY_COINS]) + ul_coins: address[MAX_METAREGISTRY_COINS] = self._get_underlying_coins(_pool) + + for i in range(MAX_METAREGISTRY_COINS): + + if ul_coins[i] == empty(address): + break + + if i < base_coin_idx: + ul_balance = CurvePool(_pool).balances(i) + + else: + + if base_pool == legacy_pool: + ul_balance = CurveLegacyPool(base_pool).balances(convert(i - base_coin_idx, int128)) + else: + ul_balance = CurvePool(base_pool).balances(i - base_coin_idx) + ul_balance = ul_balance * underlying_pct / 10**36 + underlying_balances[i] = ul_balance + + return underlying_balances + + +@internal +@view +def _pad_uint_dynarray( + _array: DynArray[uint256, MAX_METAREGISTRY_COINS] +) -> uint256[MAX_METAREGISTRY_COINS]: + _padded_array: uint256[MAX_METAREGISTRY_COINS] = empty(uint256[MAX_METAREGISTRY_COINS]) + array_len: uint256 = len(_array) + for i in range(MAX_METAREGISTRY_COINS): + if i == array_len: + break + _padded_array[i] = _array[i] + return _padded_array + + +@internal +@view +def _pad_addr_dynarray(_array: DynArray[address, MAX_METAREGISTRY_COINS]) -> address[MAX_METAREGISTRY_COINS]: + _padded_array: address[MAX_METAREGISTRY_COINS] = empty(address[MAX_METAREGISTRY_COINS]) + array_len: uint256 = len(_array) + for i in range(MAX_METAREGISTRY_COINS): + if i == array_len: + break + _padded_array[i] = _array[i] + return _padded_array + + +@internal +@view +def _get_gauge_type(_gauge: address) -> int128: + + success: bool = False + response: Bytes[32] = b"" + success, response = raw_call( + GAUGE_CONTROLLER, + concat( + method_id("gauge_types(address)"), + convert(_gauge, bytes32), + ), + max_outsize=32, + revert_on_failure=False, + is_static_call=True + ) + + if success and not Gauge(_gauge).is_killed(): + return convert(response, int128) + + return 0 + + +# ---- view methods (API) of the contract ---- # +@external +@view +def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: + return self.base_registry.find_pool_for_coins(_from, _to, i) + + +@external +@view +def get_admin_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Get the balances of the admin of the pool + @dev does not use base registry admin_balances because that has errors + in the getter for n_coins (some pools show zero, so admin balances is zero) + @param _pool address of the pool + @return balances of the admin of the pool + """ + n_coins: uint256 = self.base_registry.get_n_coins(_pool) + admin_balances: uint256[MAX_METAREGISTRY_COINS] = empty(uint256[MAX_METAREGISTRY_COINS]) + for i in range(MAX_METAREGISTRY_COINS): + if i == n_coins: + break + admin_balances[i] = CurvePool(_pool).admin_balances(i) + return admin_balances + + +@external +@view +def get_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Get the balances of the pool + @param _pool address of the pool + @return balances of the pool + """ + return self._pad_uint_dynarray(self.base_registry.get_balances(_pool)) + + +@external +@view +def get_base_pool(_pool: address) -> address: + """ + @notice Get the base pool of the pool + @param _pool address of the pool + @return base pool of the pool + """ + return self.base_registry.get_base_pool(_pool) + + +@view +@external +def get_coin_indices(_pool: address, _from: address, _to: address) -> (int128, int128, bool): + """ + @notice Get the indices of the coins in the pool + @param _pool address of the pool + @param _from address of the coin + @param _to address of the coin + @return coin indices and whether the coin swap involves an underlying market or not + """ + coin1: int128 = 0 + coin2: int128 = 0 + is_underlying: bool = False + + (coin1, coin2) = self.base_registry.get_coin_indices(_pool, _from, _to) + + # due to a bug in original factory contract, `is_underlying`` is always True + # to fix this, we first check if it is a metapool, and if not then we return + # False. If so, then we check if basepool lp token is one of the two coins, + # in which case `is_underlying` would be False + if self.base_registry.is_meta(_pool): + base_pool_lp_token: address = self.base_registry.get_coins(_pool)[1] + if base_pool_lp_token not in [_from, _to]: + is_underlying = True + + return (coin1, coin2, is_underlying) + + +@external +@view +def get_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + """ + @notice Get the coins of the pool + @param _pool address of the pool + @return coins of the pool + """ + return self._get_coins(_pool) + + +@external +@view +def get_decimals(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Get the decimals of coins in the pool + @param _pool address of the pool + @return decimals of coins in the pool + """ + return self._pad_uint_dynarray(self.base_registry.get_decimals(_pool)) + + +@external +@view +def get_fees(_pool: address) -> uint256[10]: + """ + @notice Get the fees of the pool + @param _pool address of the pool + @return fees of the pool + """ + fees: uint256[10] = empty(uint256[10]) + fees[0] = CurvePool(_pool).fee() + fees[1] = CurvePool(_pool).admin_fee() + fees[2] = CurvePool(_pool).offpeg_fee_multiplier() + return fees + + +@external +@view +def get_virtual_price_from_lp_token(_pool: address) -> uint256: + """ + @notice Get the virtual price of the pool + @param _pool address of the pool + @return virtual price of the pool + """ + return CurvePool(_pool).get_virtual_price() + + +@external +@view +def get_gauges(_pool: address) -> (address[10], int128[10]): + """ + @notice Get the gauges and gauge types of the pool + @param _pool address of the pool + @return gauges of the pool + """ + gauges: address[10] = empty(address[10]) + types: int128[10] = empty(int128[10]) + gauges[0] = self.base_registry.get_gauge(_pool) + types[0] = self._get_gauge_type(gauges[0]) + return (gauges, types) + + +@external +@view +def get_lp_token(_pool: address) -> address: + """ + @notice Get the lp token of the pool + @dev for stableswap factory pools, the pool is the lp token itself + @param _pool address of the pool + @return lp token of the pool + """ + return _pool + + +@external +@view +def get_n_coins(_pool: address) -> uint256: + """ + @notice Get the number of coins in the pool + @param _pool address of the pool + @return number of coins in the pool + """ + return self.base_registry.get_n_coins(_pool) + + +@external +@view +def get_n_underlying_coins(_pool: address) -> uint256: + """ + @notice Get the number of underlying coins in the pool + @param _pool address of the pool + @return number of underlying coins in the pool + """ + # need to check if any of the token is a base pool LP token + # since a metapool can be lptoken:lptoken, and it would count + # underlying coins as 1 + base_pool_n_coins instead of 2 x base_pool_n_coins + coins: address[MAX_METAREGISTRY_COINS] = self._get_coins(_pool) + base_pool: address = empty(address) + num_coins: uint256 = 0 + for i in range(MAX_METAREGISTRY_COINS): + + if coins[i] == empty(address): + break + + base_pool = self.base_registry.get_base_pool(coins[i]) + if base_pool == empty(address) and coins[i] != empty(address): + num_coins += 1 + else: + num_coins += self.base_registry.base_pool_data(base_pool).n_coins + + return num_coins + + +@external +@view +def get_pool_asset_type(_pool: address) -> uint256: + """ + @notice Get the asset type of the coins in the pool + @dev 0 = USD, 1 = ETH, 2 = BTC, 3 = Other + @param _pool address of the pool + @return pool asset type of the pool + """ + return self.base_registry.get_pool_asset_type(_pool) + + +@external +@view +def get_pool_from_lp_token(_lp_token: address) -> address: + """ + @notice Get the pool of the lp token + @dev This is more or less like a pass through method. Can be ignored but + We leave it in for consistency across registry handlers. + @param _lp_token address of the lp token (which is also the pool) + @return pool of the lp token + """ + if self.base_registry.get_n_coins(_lp_token) > 0: + return _lp_token + return empty(address) + + +@external +@view +def get_pool_name(_pool: address) -> String[64]: + """ + @notice Get the name of the pool + @dev stable factory pools are ERC20 tokenized + @return name of the pool + """ + if self.base_registry.get_n_coins(_pool) == 0: + # _pool is not in base registry, so we ignore: + return "" + return ERC20(_pool).name() + + +@external +@view +def get_pool_params(_pool: address) -> uint256[20]: + """ + @notice Get the parameters of the pool + @param _pool address of the pool + @return parameters of the pool + """ + stableswap_pool_params: uint256[20] = empty(uint256[20]) + stableswap_pool_params[0] = self.base_registry.get_A(_pool) + return stableswap_pool_params + + +@external +@view +def get_underlying_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Get the underlying balances of the pool + @param _pool address of the pool + @return underlying balances of the pool + """ + if not self.base_registry.is_meta(_pool): + return self._pad_uint_dynarray(self.base_registry.get_balances(_pool)) + return self._get_meta_underlying_balances(_pool) + + +@external +@view +def get_underlying_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + """ + @notice Get the underlying coins of the pool + @param _pool address of the pool + @return underlying coins of the pool + """ + if not self.base_registry.is_meta(_pool): + return self._get_coins(_pool) + return self._get_underlying_coins(_pool) + + +@external +@view +def get_underlying_decimals(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Get the underlying decimals of the pool + @dev If it is a metapool, method uses the base registry. Else it uses a + custom getter. This is because the base registry cannot unpack decimals + (stored as a bitmap) if there is no metapool. So it returns the decimals of + only the first coin. + @param _pool Address of the pool + @return underlying decimals of the pool + """ + if not self.base_registry.is_meta(_pool): + return self._pad_uint_dynarray(self.base_registry.get_decimals(_pool)) + return self._pad_uint_dynarray(self.base_registry.get_underlying_decimals(_pool)) + + +@external +@view +def is_meta(_pool: address) -> bool: + """ + @notice Check if the pool is a metapool + @param _pool address of the pool + @return True if the pool is a metapool + """ + return self.base_registry.is_meta(_pool) + + +@external +@view +def is_registered(_pool: address) -> bool: + """ + @notice Check if a pool belongs to the registry using get_n_coins + @param _pool The address of the pool + @return A bool corresponding to whether the pool belongs or not + """ + return self.base_registry.get_n_coins(_pool) > 0 + + +@external +@view +def pool_count() -> uint256: + """ + @notice Get the number of pools in the registry + @return number of pools in the registry + """ + return self.base_registry.pool_count() + + +@external +@view +def pool_list(_index: uint256) -> address: + """ + @notice Get the address of the pool at the given index + @param _index The index of the pool + @return The address of the pool + """ + return self.base_registry.pool_list(_index) diff --git a/contracts/mainnet/registry_handlers/ng/CurveTricryptoFactoryHandler.vy b/contracts/mainnet/registry_handlers/ng/CurveTricryptoFactoryHandler.vy new file mode 100644 index 0000000..86b923e --- /dev/null +++ b/contracts/mainnet/registry_handlers/ng/CurveTricryptoFactoryHandler.vy @@ -0,0 +1,487 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveTricryptoFactoryHandler +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice TricryptoNG Registry Handler for the MetaRegistry +""" + +interface BaseRegistry: + def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: view + def get_coin_indices(_pool: address, _from: address, _to: address) -> (uint256, uint256): view + def get_balances(_pool: address) -> uint256[MAX_COINS]: view + def get_coins(_pool: address) -> address[MAX_COINS]: view + def get_decimals(_pool: address) -> uint256[MAX_COINS]: view + def get_gauge(_pool: address) -> address: view + def get_n_coins(_pool: address) -> uint256: view + def get_token(_pool: address) -> address: view + def pool_count() -> uint256: view + def pool_list(pool_id: uint256) -> address: view + +interface TricryptoNG: + def adjustment_step() -> uint256: view + def ADMIN_FEE() -> uint256: view + def allowed_extra_profit() -> uint256: view + def A() -> uint256: view + def balances(i: uint256) -> uint256: view + def D() -> uint256: view + def fee() -> uint256: view + def fee_gamma() -> uint256: view + def gamma() -> uint256: view + def get_virtual_price() -> uint256: view + def ma_time() -> uint256: view + def mid_fee() -> uint256: view + def out_fee() -> uint256: view + def virtual_price() -> uint256: view + def xcp_profit() -> uint256: view + def xcp_profit_a() -> uint256: view + +interface ERC20: + def name() -> String[64]: view + def balanceOf(_addr: address) -> uint256: view + def totalSupply() -> uint256: view + def decimals() -> uint256: view + +interface GaugeController: + def gauge_types(gauge: address) -> int128: view + def gauges(i: uint256) -> address: view + +interface Gauge: + def is_killed() -> bool: view + + +# ---- constants ---- # +GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB +MAX_COINS: constant(uint256) = 3 +MAX_METAREGISTRY_COINS: constant(uint256) = 8 +MAX_POOLS: constant(uint256) = 65536 +N_COINS: constant(uint256) = 3 + + +# ---- storage variables ---- # +base_registry: public(BaseRegistry) + + +# ---- constructor ---- # +@external +def __init__(_registry_address: address): + self.base_registry = BaseRegistry(_registry_address) + + +# ---- internal methods ---- # +@internal +@view +def _pad_uint_array(_array: uint256[MAX_COINS]) -> uint256[MAX_METAREGISTRY_COINS]: + _padded_array: uint256[MAX_METAREGISTRY_COINS] = empty(uint256[MAX_METAREGISTRY_COINS]) + for i in range(MAX_COINS): + _padded_array[i] = _array[i] + return _padded_array + + +@internal +@view +def _get_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + return self._pad_uint_array(self.base_registry.get_balances(_pool)) + + +@internal +@view +def _get_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + _coins: address[MAX_COINS] = self.base_registry.get_coins(_pool) + _padded_coins: address[MAX_METAREGISTRY_COINS] = empty(address[MAX_METAREGISTRY_COINS]) + for i in range(MAX_COINS): + _padded_coins[i] = _coins[i] + return _padded_coins + + +@internal +@view +def _get_decimals(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + return self._pad_uint_array(self.base_registry.get_decimals(_pool)) + + +@internal +@view +def _get_n_coins(_pool: address) -> uint256: + + if (self.base_registry.get_coins(_pool)[0] != empty(address)): + return N_COINS + return 0 + + +@internal +@view +def _get_gauge_type(_gauge: address) -> int128: + + # try to get gauge type registered in gauge controller + success: bool = False + response: Bytes[32] = b"" + success, response = raw_call( + GAUGE_CONTROLLER, + concat( + method_id("gauge_types(address)"), + convert(_gauge, bytes32), + ), + max_outsize=32, + revert_on_failure=False, + is_static_call=True + ) + + if success and not Gauge(_gauge).is_killed(): + return convert(response, int128) + + # if we are here, the call to get gauge_type failed. + # in such a case, return a default value. + # ethereum: mainnet crypto pools have gauge type 5 + return 5 + + +@internal +@view +def _is_registered(_pool: address) -> bool: + return self._get_n_coins(_pool) > 0 + + +# ---- view methods (API) of the contract ---- # +@external +@view +def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: + """ + @notice checks if either of the two coins are in a base pool and then checks + if the basepool lp token and the other coin have a pool. + This is done because the factory does not have `underlying` methods in + pools that have a basepool lp token in them + @param _from Address of the _from coin + @param _to Address of the _to coin + @param i Index of the pool to return + @return Address of the pool + """ + return self.base_registry.find_pool_for_coins(_from, _to, i) + + +@external +@view +def get_admin_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the balances of the admin tokens of the given pool + @dev Cryptoswap pools do not store admin fees in the form of + admin token balances. Instead, the admin fees are computed + at the time of claim iff sufficient profits have been made. + These fees are allocated to the admin by minting LP tokens + (dilution). The logic to calculate fees are derived from + cryptopool._claim_admin_fees() method. + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of admin balances + """ + + xcp_profit: uint256 = TricryptoNG(_pool).xcp_profit() + xcp_profit_a: uint256 = TricryptoNG(_pool).xcp_profit_a() + admin_fee: uint256 = TricryptoNG(_pool).ADMIN_FEE() + admin_balances: uint256[MAX_METAREGISTRY_COINS] = empty(uint256[MAX_METAREGISTRY_COINS]) + + # admin balances are non zero if pool has made more than allowed profits: + if xcp_profit > xcp_profit_a: + + # calculate admin fees in lp token amounts: + fees: uint256 = (xcp_profit - xcp_profit_a) * admin_fee / (2 * 10**10) + if fees > 0: + vprice: uint256 = TricryptoNG(_pool).virtual_price() + frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18 + + # the total supply of lp token is current supply + claimable: + lp_token_total_supply: uint256 = ERC20(_pool).totalSupply() + d_supply: uint256 = lp_token_total_supply * frac / 10**18 + lp_token_total_supply += d_supply + admin_lp_frac: uint256 = d_supply * 10 ** 18 / lp_token_total_supply + + # get admin balances in individual assets: + reserves: uint256[MAX_METAREGISTRY_COINS] = self._get_balances(_pool) + for i in range(MAX_METAREGISTRY_COINS): + admin_balances[i] = admin_lp_frac * reserves[i] / 10 ** 18 + + return admin_balances + + +@external +@view +def get_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the balances of the tokens of the given pool + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of balances + """ + return self._get_balances(_pool) + + +@external +@view +def get_base_pool(_pool: address) -> address: + """ + @notice Returns the base pool of the given pool + @dev Returns empty(address) if the pool isn't a metapool + @param _pool Address of the pool + @return Address of the base pool + """ + return empty(address) + + +@external +@view +def get_coin_indices(_pool: address, _from: address, _to: address) -> (uint256, uint256, bool): + """ + @notice Convert coin addresses to indices for use with pool methods + @param _pool Address of the pool + @param _from Address of the from coin + @param _to Address of the to coin + @return (uint256, uint256, bool) Tuple of indices of the coins in the pool, + and whether the market is an underlying market or not. + """ + i: uint256 = 0 + j: uint256 = 0 + + (i, j) = self.base_registry.get_coin_indices(_pool, _from, _to) + + return (i, j, False) + + +@external +@view +def get_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the coins of the given pool + @param _pool Address of the pool + @return address[MAX_METAREGISTRY_COINS] Array of coins + """ + return self._get_coins(_pool) + + +@external +@view +def get_decimals(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the decimals of the coins in a given pool + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of decimals + """ + return self._get_decimals(_pool) + + +@external +@view +def get_fees(_pool: address) -> uint256[10]: + """ + @notice Returns the fees of the given pool + @param _pool Address of the pool + @return uint256[10] Array of fees. Fees are arranged as: + 1. swap fee (or `fee`) + 2. admin fee + 3. mid fee (fee when cryptoswap pool is pegged) + 4. out fee (fee when cryptoswap pool depegs) + """ + fees: uint256[10] = empty(uint256[10]) + pool_fees: uint256[4] = [ + TricryptoNG(_pool).fee(), + TricryptoNG(_pool).ADMIN_FEE(), + TricryptoNG(_pool).mid_fee(), + TricryptoNG(_pool).out_fee(), + ] + for i in range(4): + fees[i] = pool_fees[i] + return fees + + +@external +@view +def get_gauges(_pool: address) -> (address[10], int128[10]): + """ + @notice Returns the gauges of the given pool + @param _pool Address of the pool + @return (address[10], int128[10]) Tuple of gauges. Gauges are arranged as: + 1. gauge addresses + 2. gauge types + """ + gauges: address[10] = empty(address[10]) + types: int128[10] = empty(int128[10]) + gauges[0] = self.base_registry.get_gauge(_pool) + types[0] = self._get_gauge_type(gauges[0]) + return (gauges, types) + + +@external +@view +def get_lp_token(_pool: address) -> address: + """ + @notice Returns the Liquidity Provider token of the given pool + @param _pool Address of the pool + @return Address of the Liquidity Provider token + """ + return _pool + + +@external +@view +def get_n_coins(_pool: address) -> uint256: + """ + @notice Returns the number of coins in the given pool + @param _pool Address of the pool + @return uint256 Number of coins + """ + return self._get_n_coins(_pool) + + +@external +@view +def get_n_underlying_coins(_pool: address) -> uint256: + """ + @notice Get the number of underlying coins in a pool + @param _pool Address of the pool + @return uint256 Number of underlying coins + """ + _coins: address[MAX_METAREGISTRY_COINS] = self._get_coins(_pool) + + for i in range(MAX_METAREGISTRY_COINS): + if _coins[i] == empty(address): + return i + raise + +@external +@view +def get_pool_asset_type(_pool: address) -> uint256: + """ + @notice Returns the asset type of the given pool + @dev Returns 4: 0 = USD, 1 = ETH, 2 = BTC, 3 = Other + @param _pool Address of the pool + @return uint256 Asset type + """ + return 3 + + +@external +@view +def get_pool_from_lp_token(_lp_token: address) -> address: + """ + @notice Returns the pool of the given Liquidity Provider token + @param _lp_token Address of the Liquidity Provider token + @return Address of the pool + """ + if self._get_n_coins(_lp_token) > 0: + return _lp_token + return empty(address) + + +@external +@view +def get_pool_name(_pool: address) -> String[64]: + """ + @notice Returns the name of the given pool + @param _pool Address of the pool + @return String[64] Name of the pool + """ + return ERC20(self.base_registry.get_token(_pool)).name() + +@external +@view +def get_pool_params(_pool: address) -> uint256[20]: + """ + @notice returns pool params given a cryptopool address + @dev contains all settable parameter that alter the pool's performance + @dev only applicable for cryptopools + @param _pool Address of the pool for which data is being queried. + """ + pool_params: uint256[20] = empty(uint256[20]) + pool_params[0] = TricryptoNG(_pool).A() + pool_params[1] = TricryptoNG(_pool).D() + pool_params[2] = TricryptoNG(_pool).gamma() + pool_params[3] = TricryptoNG(_pool).allowed_extra_profit() + pool_params[4] = TricryptoNG(_pool).fee_gamma() + pool_params[5] = TricryptoNG(_pool).adjustment_step() + pool_params[6] = TricryptoNG(_pool).ma_time() + return pool_params + + +@external +@view +def get_underlying_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the underlying balances of the given pool + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of underlying balances + """ + return self._get_balances(_pool) + + +@external +@view +def get_underlying_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the underlying coins of the given pool + @param _pool Address of the pool + @return address[MAX_METAREGISTRY_COINS] Array of underlying coins + """ + return self._get_coins(_pool) + + +@external +@view +def get_underlying_decimals(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the underlying decimals of the given pool + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of underlying decimals + """ + return self._get_decimals(_pool) + + +@external +@view +def get_virtual_price_from_lp_token(_token: address) -> uint256: + """ + @notice Returns the virtual price of the given Liquidity Provider token + @param _token Address of the Liquidity Provider token + @return uint256 Virtual price + """ + return TricryptoNG(_token).get_virtual_price() + + +@external +@view +def is_meta(_pool: address) -> bool: + """ + @notice Returns whether the given pool is a meta pool + @param _pool Address of the pool + @return bool Whether the pool is a meta pool + """ + return False + + +@external +@view +def is_registered(_pool: address) -> bool: + """ + @notice Check if a pool belongs to the registry using get_n_coins + @param _pool The address of the pool + @return A bool corresponding to whether the pool belongs or not + """ + return self._get_n_coins(_pool) > 0 + + +@external +@view +def pool_count() -> uint256: + """ + @notice Returns the number of pools in the registry + @return uint256 Number of pools + """ + return self.base_registry.pool_count() + + +@external +@view +def pool_list(_index: uint256) -> address: + """ + @notice Returns the address of the pool at the given index + @param _index Index of the pool + @return Address of the pool + """ + return self.base_registry.pool_list(_index) diff --git a/contracts/mainnet/registry_handlers/ng/CurveTwocryptoFactoryHandler.vy b/contracts/mainnet/registry_handlers/ng/CurveTwocryptoFactoryHandler.vy new file mode 100644 index 0000000..3878c60 --- /dev/null +++ b/contracts/mainnet/registry_handlers/ng/CurveTwocryptoFactoryHandler.vy @@ -0,0 +1,474 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveTwocryptoFactoryHandler +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved +@notice TwocryptoNG Registry Handler for the MetaRegistry +""" + +interface BaseRegistry: + def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: view + def get_coin_indices(_pool: address, _from: address, _to: address) -> (uint256, uint256): view + def get_balances(_pool: address) -> uint256[MAX_COINS]: view + def get_coins(_pool: address) -> address[MAX_COINS]: view + def get_decimals(_pool: address) -> uint256[MAX_COINS]: view + def get_gauge(_pool: address) -> address: view + def get_n_coins(_pool: address) -> uint256: view + def get_token(_pool: address) -> address: view + def pool_count() -> uint256: view + def pool_list(pool_id: uint256) -> address: view + +interface TricryptoNG: + def adjustment_step() -> uint256: view + def ADMIN_FEE() -> uint256: view + def allowed_extra_profit() -> uint256: view + def A() -> uint256: view + def balances(i: uint256) -> uint256: view + def D() -> uint256: view + def fee() -> uint256: view + def fee_gamma() -> uint256: view + def gamma() -> uint256: view + def get_virtual_price() -> uint256: view + def ma_time() -> uint256: view + def mid_fee() -> uint256: view + def out_fee() -> uint256: view + def virtual_price() -> uint256: view + def xcp_profit() -> uint256: view + def xcp_profit_a() -> uint256: view + +interface ERC20: + def name() -> String[64]: view + def balanceOf(_addr: address) -> uint256: view + def totalSupply() -> uint256: view + def decimals() -> uint256: view + +interface GaugeController: + def gauge_types(gauge: address) -> int128: view + def gauges(i: uint256) -> address: view + +interface Gauge: + def is_killed() -> bool: view + + +# ---- constants ---- # +GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB +MAX_COINS: constant(uint256) = 2 +MAX_METAREGISTRY_COINS: constant(uint256) = 8 +MAX_POOLS: constant(uint256) = 65536 +N_COINS: constant(uint256) = 2 + + +# ---- storage variables ---- # +base_registry: public(BaseRegistry) + + +# ---- constructor ---- # +@external +def __init__(_registry_address: address): + self.base_registry = BaseRegistry(_registry_address) + + +# ---- internal methods ---- # +@internal +@view +def _pad_uint_array(_array: uint256[MAX_COINS]) -> uint256[MAX_METAREGISTRY_COINS]: + _padded_array: uint256[MAX_METAREGISTRY_COINS] = empty(uint256[MAX_METAREGISTRY_COINS]) + for i in range(MAX_COINS): + _padded_array[i] = _array[i] + return _padded_array + + +@internal +@view +def _get_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + return self._pad_uint_array(self.base_registry.get_balances(_pool)) + + +@internal +@view +def _get_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + _coins: address[MAX_COINS] = self.base_registry.get_coins(_pool) + _padded_coins: address[MAX_METAREGISTRY_COINS] = empty(address[MAX_METAREGISTRY_COINS]) + for i in range(MAX_COINS): + _padded_coins[i] = _coins[i] + return _padded_coins + + +@internal +@view +def _get_decimals(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + return self._pad_uint_array(self.base_registry.get_decimals(_pool)) + + +@internal +@view +def _get_gauge_type(_gauge: address) -> int128: + + # try to get gauge type registered in gauge controller + success: bool = False + response: Bytes[32] = b"" + success, response = raw_call( + GAUGE_CONTROLLER, + concat( + method_id("gauge_types(address)"), + convert(_gauge, bytes32), + ), + max_outsize=32, + revert_on_failure=False, + is_static_call=True + ) + + if success and not Gauge(_gauge).is_killed(): + return convert(response, int128) + + # if we are here, the call to get gauge_type failed. + # in such a case, return a default value. + # ethereum: mainnet crypto pools have gauge type 5 + return 5 + + +@internal +@view +def _is_registered(_pool: address) -> bool: + return self._get_coins(_pool)[0] != empty(address) + + +# ---- view methods (API) of the contract ---- # +@external +@view +def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: + """ + @notice checks if either of the two coins are in a base pool and then checks + if the basepool lp token and the other coin have a pool. + This is done because the factory does not have `underlying` methods in + pools that have a basepool lp token in them + @param _from Address of the _from coin + @param _to Address of the _to coin + @param i Index of the pool to return + @return Address of the pool + """ + return self.base_registry.find_pool_for_coins(_from, _to, i) + + +@external +@view +def get_admin_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the balances of the admin tokens of the given pool + @dev Cryptoswap pools do not store admin fees in the form of + admin token balances. Instead, the admin fees are computed + at the time of claim iff sufficient profits have been made. + These fees are allocated to the admin by minting LP tokens + (dilution). The logic to calculate fees are derived from + cryptopool._claim_admin_fees() method. + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of admin balances + """ + + xcp_profit: uint256 = TricryptoNG(_pool).xcp_profit() + xcp_profit_a: uint256 = TricryptoNG(_pool).xcp_profit_a() + admin_fee: uint256 = TricryptoNG(_pool).ADMIN_FEE() + admin_balances: uint256[MAX_METAREGISTRY_COINS] = empty(uint256[MAX_METAREGISTRY_COINS]) + + # admin balances are non zero if pool has made more than allowed profits: + if xcp_profit > xcp_profit_a: + + # calculate admin fees in lp token amounts: + fees: uint256 = (xcp_profit - xcp_profit_a) * admin_fee / (2 * 10**10) + if fees > 0: + vprice: uint256 = TricryptoNG(_pool).virtual_price() + frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18 + + # the total supply of lp token is current supply + claimable: + lp_token_total_supply: uint256 = ERC20(_pool).totalSupply() + d_supply: uint256 = lp_token_total_supply * frac / 10**18 + lp_token_total_supply += d_supply + admin_lp_frac: uint256 = d_supply * 10 ** 18 / lp_token_total_supply + + # get admin balances in individual assets: + reserves: uint256[MAX_METAREGISTRY_COINS] = self._get_balances(_pool) + for i in range(MAX_METAREGISTRY_COINS): + admin_balances[i] = admin_lp_frac * reserves[i] / 10 ** 18 + + return admin_balances + + +@external +@view +def get_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the balances of the tokens of the given pool + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of balances + """ + return self._get_balances(_pool) + + +@external +@view +def get_base_pool(_pool: address) -> address: + """ + @notice Returns the base pool of the given pool + @dev Returns empty(address) if the pool isn't a metapool + @param _pool Address of the pool + @return Address of the base pool + """ + return empty(address) + + +@external +@view +def get_coin_indices(_pool: address, _from: address, _to: address) -> (uint256, uint256, bool): + """ + @notice Convert coin addresses to indices for use with pool methods + @param _pool Address of the pool + @param _from Address of the from coin + @param _to Address of the to coin + @return (uint256, uint256, bool) Tuple of indices of the coins in the pool, + and whether the market is an underlying market or not. + """ + i: uint256 = 0 + j: uint256 = 0 + + (i, j) = self.base_registry.get_coin_indices(_pool, _from, _to) + + return (i, j, False) + + +@external +@view +def get_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the coins of the given pool + @param _pool Address of the pool + @return address[MAX_METAREGISTRY_COINS] Array of coins + """ + return self._get_coins(_pool) + + +@external +@view +def get_decimals(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the decimals of the coins in a given pool + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of decimals + """ + return self._get_decimals(_pool) + + +@external +@view +def get_fees(_pool: address) -> uint256[10]: + """ + @notice Returns the fees of the given pool + @param _pool Address of the pool + @return uint256[10] Array of fees. Fees are arranged as: + 1. swap fee (or `fee`) + 2. admin fee + 3. mid fee (fee when cryptoswap pool is pegged) + 4. out fee (fee when cryptoswap pool depegs) + """ + fees: uint256[10] = empty(uint256[10]) + pool_fees: uint256[4] = [ + TricryptoNG(_pool).fee(), + TricryptoNG(_pool).ADMIN_FEE(), + TricryptoNG(_pool).mid_fee(), + TricryptoNG(_pool).out_fee(), + ] + for i in range(4): + fees[i] = pool_fees[i] + return fees + + +@external +@view +def get_gauges(_pool: address) -> (address[10], int128[10]): + """ + @notice Returns the gauges of the given pool + @param _pool Address of the pool + @return (address[10], int128[10]) Tuple of gauges. Gauges are arranged as: + 1. gauge addresses + 2. gauge types + """ + gauges: address[10] = empty(address[10]) + types: int128[10] = empty(int128[10]) + gauges[0] = self.base_registry.get_gauge(_pool) + types[0] = self._get_gauge_type(gauges[0]) + return (gauges, types) + + +@external +@view +def get_lp_token(_pool: address) -> address: + """ + @notice Returns the Liquidity Provider token of the given pool + @param _pool Address of the pool + @return Address of the Liquidity Provider token + """ + return _pool + + +@external +@view +def get_n_coins(_pool: address) -> uint256: + """ + @notice Returns the number of coins in the given pool + @param _pool Address of the pool + @return uint256 Number of coins + """ + return 2 + + +@external +@view +def get_n_underlying_coins(_pool: address) -> uint256: + """ + @notice Get the number of underlying coins in a pool + @param _pool Address of the pool + @return uint256 Number of underlying coins + """ + return 2 + + +@external +@view +def get_pool_asset_type(_pool: address) -> uint256: + """ + @notice Returns the asset type of the given pool + @dev Returns 4: 0 = USD, 1 = ETH, 2 = BTC, 3 = Other + @param _pool Address of the pool + @return uint256 Asset type + """ + return 3 + + +@external +@view +def get_pool_from_lp_token(_lp_token: address) -> address: + """ + @notice Returns the pool of the given Liquidity Provider token + @param _lp_token Address of the Liquidity Provider token + @return Address of the pool + """ + if self._is_registered(_lp_token): + return _lp_token + return empty(address) + + +@external +@view +def get_pool_name(_pool: address) -> String[64]: + """ + @notice Returns the name of the given pool + @param _pool Address of the pool + @return String[64] Name of the pool + """ + return ERC20(self.base_registry.get_token(_pool)).name() + +@external +@view +def get_pool_params(_pool: address) -> uint256[20]: + """ + @notice returns pool params given a cryptopool address + @dev contains all settable parameter that alter the pool's performance + @dev only applicable for cryptopools + @param _pool Address of the pool for which data is being queried. + """ + pool_params: uint256[20] = empty(uint256[20]) + pool_params[0] = TricryptoNG(_pool).A() + pool_params[1] = TricryptoNG(_pool).D() + pool_params[2] = TricryptoNG(_pool).gamma() + pool_params[3] = TricryptoNG(_pool).allowed_extra_profit() + pool_params[4] = TricryptoNG(_pool).fee_gamma() + pool_params[5] = TricryptoNG(_pool).adjustment_step() + pool_params[6] = TricryptoNG(_pool).ma_time() + return pool_params + + +@external +@view +def get_underlying_balances(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the underlying balances of the given pool + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of underlying balances + """ + return self._get_balances(_pool) + + +@external +@view +def get_underlying_coins(_pool: address) -> address[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the underlying coins of the given pool + @param _pool Address of the pool + @return address[MAX_METAREGISTRY_COINS] Array of underlying coins + """ + return self._get_coins(_pool) + + +@external +@view +def get_underlying_decimals(_pool: address) -> uint256[MAX_METAREGISTRY_COINS]: + """ + @notice Returns the underlying decimals of the given pool + @param _pool Address of the pool + @return uint256[MAX_METAREGISTRY_COINS] Array of underlying decimals + """ + return self._get_decimals(_pool) + + +@external +@view +def get_virtual_price_from_lp_token(_token: address) -> uint256: + """ + @notice Returns the virtual price of the given Liquidity Provider token + @param _token Address of the Liquidity Provider token + @return uint256 Virtual price + """ + return TricryptoNG(_token).get_virtual_price() + + +@external +@view +def is_meta(_pool: address) -> bool: + """ + @notice Returns whether the given pool is a meta pool + @param _pool Address of the pool + @return bool Whether the pool is a meta pool + """ + return False + + +@external +@view +def is_registered(_pool: address) -> bool: + """ + @notice Check if a pool belongs to the registry using get_n_coins + @param _pool The address of the pool + @return A bool corresponding to whether the pool belongs or not + """ + return self._is_registered(_pool) + + +@external +@view +def pool_count() -> uint256: + """ + @notice Returns the number of pools in the registry + @return uint256 Number of pools + """ + return self.base_registry.pool_count() + + +@external +@view +def pool_list(_index: uint256) -> address: + """ + @notice Returns the address of the pool at the given index + @param _index Index of the pool + @return Address of the pool + """ + return self.base_registry.pool_list(_index) diff --git a/contracts/mocks/ERC20.vy b/contracts/mocks/ERC20.vy new file mode 100644 index 0000000..614781e --- /dev/null +++ b/contracts/mocks/ERC20.vy @@ -0,0 +1,74 @@ +# pragma version 0.3.10 + +""" +@notice Mock ERC20 for testing +""" + + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + +name: public(String[64]) +symbol: public(String[32]) +decimals: public(uint256) +balanceOf: public(HashMap[address, uint256]) +allowances: HashMap[address, HashMap[address, uint256]] +totalSupply: public(uint256) + +# asset type +asset_type: public(constant(uint8)) = 0 + + +@external +def __init__(_name: String[64], _symbol: String[32], _decimals: uint256): + self.name = _name + self.symbol = _symbol + self.decimals = _decimals + + +@external +@view +def allowance(_owner: address, _spender: address) -> uint256: + return self.allowances[_owner][_spender] + + +@external +def transfer(_to: address, _value: uint256) -> bool: + self.balanceOf[msg.sender] -= _value + self.balanceOf[_to] += _value + log Transfer(msg.sender, _to, _value) + return True + + +@external +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + self.allowances[_from][msg.sender] -= _value + log Transfer(_from, _to, _value) + return True + + +@external +def approve(_spender: address, _value: uint256) -> bool: + self.allowances[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + return True + + +@external +def _mint_for_testing(_target: address, _value: uint256) -> bool: + self.totalSupply += _value + self.balanceOf[_target] += _value + log Transfer(empty(address), _target, _value) + + return True diff --git a/contracts/mocks/ERC4626.vy b/contracts/mocks/ERC4626.vy new file mode 100644 index 0000000..6902595 --- /dev/null +++ b/contracts/mocks/ERC4626.vy @@ -0,0 +1,99 @@ +# pragma version 0.3.10 + +""" +@notice Mock ERC20 with oracle +@dev This is for testing only, it is NOT safe for use +""" + + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + +name: public(String[64]) +symbol: public(String[32]) +decimals: public(uint256) +balanceOf: public(HashMap[address, uint256]) +allowances: HashMap[address, HashMap[address, uint256]] +totalSupply: public(uint256) + +exchange_rate: public(uint256) + +# asset type +asset_type: public(constant(uint8)) = 1 + + +@external +def __init__( + _name: String[64], + _symbol: String[32], + _decimals: uint256, + _exchange_rate: uint256 +): + + self.name = _name + self.symbol = _symbol + + assert _decimals == 18, "Decimals must be 18" + self.decimals = _decimals + self.exchange_rate = _exchange_rate + + +@external +@view +def allowance(_owner: address, _spender: address) -> uint256: + return self.allowances[_owner][_spender] + + +@external +def transfer(_to: address, _value: uint256) -> bool: + self.balanceOf[msg.sender] -= _value + self.balanceOf[_to] += _value + log Transfer(msg.sender, _to, _value) + return True + + +@external +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + self.allowances[_from][msg.sender] -= _value + log Transfer(_from, _to, _value) + return True + + +@external +def approve(_spender: address, _value: uint256) -> bool: + self.allowances[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + return True + + +@external +@view +def exchangeRate() -> uint256: + rate: uint256 = self.exchange_rate + return rate + + +@external +def set_exchange_rate(rate: uint256) -> bool: + self.exchange_rate = rate + return True + + +@external +def _mint_for_testing(_target: address, _value: uint256) -> bool: + self.totalSupply += _value + self.balanceOf[_target] += _value + log Transfer(empty(address), _target, _value) + + return True diff --git a/scripts/boa_scripts/set_up_registries.py b/scripts/boa_scripts/set_up_registries.py deleted file mode 100644 index a7e8c72..0000000 --- a/scripts/boa_scripts/set_up_registries.py +++ /dev/null @@ -1,152 +0,0 @@ -# flake8: noqa - -import os -import sys -from dataclasses import dataclass -from typing import List - -import boa -import deployment_utils as deploy_utils -from boa.network import NetworkEnv -from deploy_infra import deployments -from eth_account import Account -from eth_typing import Address -from eth_utils import to_checksum_address -from rich.console import Console as RichConsole - -logger = RichConsole(file=sys.stdout) - - -def deploy_factory_handler(): - pass - - -def set_up_registries( - network: str, url: str, account: str, fork: bool = False -): - """ - Set up registries for the Curve StableSwapNG factory. - :param network: Network to deploy to. - :param url: URL to connect to. - :param account: Account to use. - :param fork: Whether to deploy to a fork (test) network. - """ - - logger.log(f"Connecting to {network} ...") - if fork: - boa.env.fork(url) - boa.env.eoa = deploy_utils.FIDDYDEPLOYER - logger.log("Forkmode") - else: - logger.log("Prodmode") - boa.set_env(NetworkEnv(url)) - boa.env.eoa = Account.from_key(os.environ[account]) - - data = next( - data - for _network, data in deploy_utils.curve_dao_network_settings.items() - if _network in network - ) - - owner = data.dao_ownership_contract - fee_receiver = data.fee_receiver_address - assert owner, f"Curve's DAO contracts may not be on {network}." - assert fee_receiver, f"Curve's DAO contracts may not be on {network}." - address_provider = Contract(data.address_provider) - - # -------------------------- Register into AddressProvider -------------------------- - - max_id = address_provider.max_id() - description = "Curve StableSwapNG" - boss = Contract(address_provider.admin()) - - # check if account can handle boss: - account_is_boss_handler = any( - account.address.lower() == boss.admins(i).lower() for i in range(2) - ) - assert account_is_boss_handler # only authorised accounts can write to address provider # noqa: E501 - - is_new_deployment = not any( - address_provider.get_id_info(i).description is description - for i in range(max_id + 1) - ) - - if is_new_deployment: - logger.info( - f"Adding a new registry provider entry at id: {max_id + 1}" - ) - - # we're adding a new id - with accounts.use_sender(account) as account: - boss.execute( - address_provider.address, - address_provider.add_new_id.encode_input(factory, description), - gas_limit=400000, - **deploy_utils._get_tx_params(), - ) - - else: - assert address_provider.get_id_info(index).description == description - - logger.info( - f"Updating existing registry provider entry at id: {index}" - ) - - # we're updating an existing id with the same description: - with accounts.use_sender(account) as account: - boss.execute( - address_provider.address, - address_provider.set_address.encode_input(index, factory), - gas_limit=200000, - **deploy_utils._get_tx_params(), - ) - - assert address_provider.get_id_info(index).addr.lower() == factory.lower() - - logger.info("AddressProvider integration complete!") - - # -------------------------- Set up metaregistry -------------------------- - - metaregistry_address = deploy_utils.curve_dao_network_settings[ - network - ].metaregistry_address - base_pool_registry_address = deploy_utils.curve_dao_network_settings[ - network - ].base_pool_registry_address - - if metaregistry_address: - metaregistry = Contract(metaregistry_address) - boss = Contract(metaregistry.owner()) - - # set up metaregistry integration: - logger.info("Integrate into Metaregistry ...") - logger.info( - "Deploying Factory handler to integrate it to the metaregistry:" - ) - factory_handler = account.deploy( - project.CurveStableSwapFactoryNGHandler, - factory.address, - base_pool_registry_address, - **deploy_utils._get_tx_params(), - ) - - boss.execute( - metaregistry.address, - metaregistry.add_registry_handler.encode_input(factory_handler), - **deploy_utils._get_tx_params(), - ) - - logger.info("Metaregistry integration complete!") - - -def main(): - set_up_registries( - "ethereum:mainnet", - os.environ["RPC_ETHEREUM"], - "FIDDYDEPLOYER", - fork=False, - ) - - -if __name__ == "__main__": - main() diff --git a/scripts/change_registry_handler.py b/scripts/change_registry_handler.py index 38e25ae..e7126d2 100644 --- a/scripts/change_registry_handler.py +++ b/scripts/change_registry_handler.py @@ -3,8 +3,8 @@ import boa from rich.console import Console as RichConsole -from scripts.constants import ADDRESS_PROVIDER, ZERO_ADDRESS from scripts.deployment_utils import get_deployed_contract, setup_environment +from scripts.utils.constants import ADDRESS_PROVIDER, ZERO_ADDRESS RICH_CONSOLE = RichConsole(file=sys.stdout) CRYPTO_REGISTRY_HANDLER = "0x5f493fEE8D67D3AE3bA730827B34126CFcA0ae94" diff --git a/scripts/deploy.py b/scripts/deploy.py index 15b8757..880a193 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -8,16 +8,16 @@ requires the URL and ACCOUNT environment variables to be set """ import boa -from constants import ( +from eth_abi import encode +from rich import Console as RichConsole + +from scripts.deployment_utils import setup_environment +from scripts.utils.constants import ( ADDRESS_PROVIDER, CRYPTO_FACTORY_ADDRESS, STABLE_FACTORY_ADDRESS, STABLE_REGISTRY_ADDRESS, ) -from eth_abi import encode -from rich import Console as RichConsole - -from scripts.deployment_utils import setup_environment def main(): diff --git a/scripts/deploy_addressprovider_and_setup copy.py b/scripts/deploy_addressprovider_and_setup.py similarity index 100% rename from scripts/deploy_addressprovider_and_setup copy.py rename to scripts/deploy_addressprovider_and_setup.py diff --git a/scripts/deploy_and_setup_crypto_ng_handler.py b/scripts/deploy_and_setup_crypto_ng_handler.py new file mode 100644 index 0000000..80b50ca --- /dev/null +++ b/scripts/deploy_and_setup_crypto_ng_handler.py @@ -0,0 +1,47 @@ +# flake8: noqa + +import os +import sys + +import boa +from boa.network import NetworkEnv +from eth_account import Account +from rich import console as rich_console + +sys.path.append("./") +from scripts.deploy_addressprovider_and_setup import fetch_url +from scripts.utils.constants import FIDDY_DEPLOYER + + +def main(network: str = "ethereum", fork: bool = True): + console = rich_console.Console() + + if not fork: + # Prodmode + console.log("Running script in prod mode...") + boa.set_env(NetworkEnv(fetch_url(network))) + boa.env.add_account(Account.from_key(os.environ["FIDDYDEPLOYER"])) + + else: + # Forkmode + console.log("Simulation Mode. Writing to mainnet-fork.") + boa.env.fork(url=fetch_url(network)) + boa.env.eoa = FIDDY_DEPLOYER + + # deploy handlers: + registry = boa.load( + "contracts/mainnet/registry_handlers/ng/CurveTwocryptoFactoryHandler.vy", + "0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F", + ) + registry = boa.load( + "contracts/mainnet/registry_handlers/ng/CurveTricryptoFactoryHandler.vy", + "0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963", + ) + console.log(f"Deployed Factory Handlers.") + + +if __name__ == "__main__": + network = "ethereum" + fork = False + + main(network, fork) diff --git a/scripts/deploy_and_setup_stableswap_factory_ng_handler.py b/scripts/deploy_and_setup_stableswap_factory_ng_handler.py new file mode 100644 index 0000000..82d025a --- /dev/null +++ b/scripts/deploy_and_setup_stableswap_factory_ng_handler.py @@ -0,0 +1,45 @@ +# flake8: noqa + +import os +import sys + +import boa +from boa.network import NetworkEnv +from eth_account import Account +from rich import console as rich_console + +sys.path.append("./") +from scripts.deploy_addressprovider_and_setup import fetch_url +from scripts.utils.constants import FIDDY_DEPLOYER + + +def main(network: str = "ethereum", fork: bool = True): + console = rich_console.Console() + + if not fork: + # Prodmode + console.log("Running script in prod mode...") + boa.set_env(NetworkEnv(fetch_url(network))) + boa.env.add_account(Account.from_key(os.environ["FIDDYDEPLOYER"])) + + else: + # Forkmode + console.log("Simulation Mode. Writing to mainnet-fork.") + boa.env.fork(url=fetch_url(network)) + boa.env.eoa = FIDDY_DEPLOYER + + # deploy basepool registry: + registry = boa.load( + "contracts/mainnet/registry_handlers/ng/CurveStableSwapFactoryNGHandler.vy", + "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf", # stableswap factory ng + ) + console.log( + f"Deployed Curve Stableswap Factory Handler to {registry.address}" + ) + + +if __name__ == "__main__": + network = "ethereum" + fork = False + + main(network, fork) diff --git a/scripts/setup_metaregistry.py b/scripts/setup_metaregistry.py index 57674e5..87111af 100644 --- a/scripts/setup_metaregistry.py +++ b/scripts/setup_metaregistry.py @@ -13,15 +13,18 @@ import boa from rich.console import Console as RichConsole -from scripts.constants import ( +from scripts.deployment_utils import get_deployed_contract, setup_environment +from scripts.utils.constants import ( ADDRESS_PROVIDER, BASE_POOLS, CRYPTO_REGISTRY_POOLS, ) -from scripts.deployment_utils import get_deployed_contract, setup_environment RICH_CONSOLE = RichConsole(file=sys.stdout) +# TODO: Metaregistry and Base Pool Registry no longer have a dependency AddressProvider's admin. +# Adjust the code accordingly: + def main(): """ diff --git a/scripts/deployment_utils.py b/scripts/utils/__init__.py similarity index 59% rename from scripts/deployment_utils.py rename to scripts/utils/__init__.py index e894cd6..d1b5f1c 100644 --- a/scripts/deployment_utils.py +++ b/scripts/utils/__init__.py @@ -5,9 +5,41 @@ from boa.network import NetworkEnv from boa.vyper.contract import VyperContract from eth_account import Account +from eth_utils import keccak from rich.console import Console as RichConsole -from scripts.constants import BASE_DIR, FIDDY_DEPLOYER +from scripts.utils.constants import BASE_DIR, FIDDY_DEPLOYER + + +def get_create2_deployment_address( + compiled_bytecode, + abi_encoded_ctor, + salt, + create2deployer, + blueprint=False, + blueprint_preamble=b"\xFE\x71\x00", +): + deployment_bytecode = compiled_bytecode + abi_encoded_ctor + if blueprint: + # Add blueprint preamble to disable calling the contract: + blueprint_bytecode = blueprint_preamble + deployment_bytecode + # Add code for blueprint deployment: + len_blueprint_bytecode = len(blueprint_bytecode).to_bytes(2, "big") + deployment_bytecode = ( + b"\x61" + + len_blueprint_bytecode + + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + blueprint_bytecode + ) + + return ( + create2deployer.computeAddress(salt, keccak(deployment_bytecode)), + deployment_bytecode, + ) + + +def deploy_via_create2_factory(deployment_bytecode, salt, create2deployer): + create2deployer.deploy(0, salt, deployment_bytecode) def setup_environment(console: RichConsole): diff --git a/scripts/class_diagram.py b/scripts/utils/class_diagram.py similarity index 100% rename from scripts/class_diagram.py rename to scripts/utils/class_diagram.py diff --git a/scripts/constants.py b/scripts/utils/constants.py similarity index 89% rename from scripts/constants.py rename to scripts/utils/constants.py index e31be7f..bcca530 100644 --- a/scripts/constants.py +++ b/scripts/utils/constants.py @@ -1,8 +1,8 @@ from os.path import abspath, dirname, join -BASE_DIR = join(dirname(abspath(__file__)), "..") +from eth.constants import ZERO_ADDRESS -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +BASE_DIR = join(dirname(abspath(__file__)), "..") ADDRESS_PROVIDER = "0x0000000022D53366457F9d5E68Ec105046FC4383" STABLE_REGISTRY_ADDRESS = "0x90E00ACe148ca3b23Ac1bC8C240C2a7Dd9c2d7f5" @@ -36,6 +36,22 @@ "is_lending": False, "is_v2": False, }, + "fraxusdp": { + "pool": "0xaE34574AC03A15cd58A92DC79De7B1A0800F1CE3", + "lp_token": "0xFC2838a17D8e8B1D5456E0a351B0708a09211147", + "num_coins": 2, + "is_legacy": False, + "is_lending": False, + "is_v2": False, + }, + "sbtcv2": { + "pool": "0xf253f83AcA21aAbD2A20553AE0BF7F65C755A07F", + "lp_token": "0x051d7e5609917Bd9b73f04BAc0DED8Dd46a74301", + "num_coins": 2, + "is_legacy": False, + "is_lending": False, + "is_v2": False, + }, } CRYPTO_REGISTRY_POOLS = { diff --git a/scripts/print_missing.py b/scripts/utils/print_missing.py similarity index 100% rename from scripts/print_missing.py rename to scripts/utils/print_missing.py diff --git a/tests/conftest.py b/tests/conftest.py index 0d2b50a..e7a90ff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,9 +27,13 @@ def _get_stable_registry_pools(): @cache def _get_stable_factory_pools(): logging.info("Retrieving stable factory pools") - return get_contract_pools( + factory_pools_mainnet = get_contract_pools( "StableFactory", "0xB9fC157394Af804a3578134A6585C0dc9cc990d4" ) + factory_ng_pools_mainnet = get_contract_pools( + "StableFactoryNG", "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf" + ) + return factory_pools_mainnet + factory_ng_pools_mainnet @cache diff --git a/tests/fixtures/accounts.py b/tests/fixtures/accounts.py index a14f5a1..5d05dcc 100644 --- a/tests/fixtures/accounts.py +++ b/tests/fixtures/accounts.py @@ -2,8 +2,8 @@ import pytest from eth_account.signers.local import LocalAccount -from scripts.constants import ADDRESS_PROVIDER from scripts.deployment_utils import get_deployed_contract +from scripts.utils.constants import ADDRESS_PROVIDER @pytest.fixture(scope="session") @@ -35,3 +35,13 @@ def owner(accounts): "AddressProvider", ADDRESS_PROVIDER ) return address_provider.admin() + + +@pytest.fixture(scope="module") +def ng_fee_receiver(): + return boa.env.generate_address() + + +@pytest.fixture(scope="module") +def ng_owner(): + return boa.env.generate_address() diff --git a/tests/fixtures/constants.py b/tests/fixtures/constants.py index a58f123..f838b66 100644 --- a/tests/fixtures/constants.py +++ b/tests/fixtures/constants.py @@ -1,6 +1,6 @@ import pytest -from scripts.constants import BASE_POOLS, CRYPTO_REGISTRY_POOLS +from scripts.utils.constants import BASE_POOLS, CRYPTO_REGISTRY_POOLS @pytest.fixture(scope="module") diff --git a/tests/fixtures/deployments.py b/tests/fixtures/deployments.py index 576d712..b11dabd 100644 --- a/tests/fixtures/deployments.py +++ b/tests/fixtures/deployments.py @@ -37,9 +37,9 @@ def crypto_factory() -> VyperContract: @pytest.fixture(scope="module") -def base_pool_registry(alice_address): +def base_pool_registry(owner): return deploy_contract( - "BasePoolRegistry", sender=alice_address, directory="registries" + "BasePoolRegistry", sender=owner, directory="registries" ) @@ -94,8 +94,8 @@ def address_provider(crypto_registry, owner): @pytest.fixture(scope="module") -def metaregistry(address_provider, owner): - return deploy_contract("MetaRegistry", address_provider, sender=owner) +def metaregistry(owner): + return deploy_contract("MetaRegistry", sender=owner) @pytest.fixture(scope="module") diff --git a/tests/fixtures/factories.py b/tests/fixtures/factories.py new file mode 100644 index 0000000..e8661b7 --- /dev/null +++ b/tests/fixtures/factories.py @@ -0,0 +1,201 @@ +import boa +import pytest + +# ---- Stableswap NG ---- # + + +@pytest.fixture() +def stableswap_ng_deployer(): + return boa.load_partial("contracts/amms/stableswapng/CurveStableSwapNG.vy") + + +@pytest.fixture() +def stableswap_ng_meta_deployer(): + return boa.load_partial( + "contracts/amms/stableswapng/CurveStableSwapMetaNG.vy" + ) + + +@pytest.fixture() +def stableswap_ng_implementation(stableswap_ng_deployer, deployer): + with boa.env.prank(deployer): + return stableswap_ng_deployer.deploy_as_blueprint() + + +@pytest.fixture() +def stableswap_ng_meta_implementation(stableswap_ng_meta_deployer, deployer): + with boa.env.prank(deployer): + return stableswap_ng_meta_deployer.deploy_as_blueprint() + + +@pytest.fixture() +def stableswap_ng_views_implementation(deployer): + with boa.env.prank(deployer): + return boa.load_partial( + "contracts/amms/stableswapng/CurveStableSwapNGViews.vy" + ).deploy() + + +@pytest.fixture() +def stableswap_ng_math_implementation(deployer): + with boa.env.prank(deployer): + return boa.load_partial( + "contracts/amms/stableswapng/CurveStableSwapNGMath.vy" + ).deploy() + + +@pytest.fixture() +def stableswap_ng_factory_empty( + deployer, + ng_fee_receiver, + ng_owner, + stableswap_ng_views_implementation, + stableswap_ng_math_implementation, + stableswap_ng_implementation, + stableswap_ng_meta_implementation, +): + with boa.env.prank(deployer): + factory = boa.load_partial( + "contracts/amms/stableswapng/CurveStableSwapFactoryNG.vy" + ).deploy(ng_fee_receiver, ng_owner) + + with boa.env.prank(ng_owner): + factory.set_views_implementation( + stableswap_ng_views_implementation.address + ) + factory.set_math_implementation( + stableswap_ng_math_implementation.address + ) + factory.set_pool_implementations( + 0, stableswap_ng_implementation.address + ) + factory.set_metapool_implementations( + 0, stableswap_ng_meta_implementation.address + ) + + return factory + + +@pytest.fixture() +def stableswap_ng_factory( + stableswap_ng_factory_empty, stableswap_ng_pool, base_pool_coins, ng_owner +): + stableswap_ng_factory_empty.add_base_pool( + stableswap_ng_pool.address, + stableswap_ng_pool.address, + [0] * len(base_pool_coins), + len(base_pool_coins), + sender=ng_owner, + ) + return stableswap_ng_factory_empty + + +# ---- Twocrypto NG ---- # + + +@pytest.fixture(scope="module") +def twocrypto_ng_math_contract(deployer): + with boa.env.prank(deployer): + return boa.load( + "contracts/amms/twocryptong/CurveCryptoMathOptimized2.vy" + ) + + +@pytest.fixture(scope="module") +def twocrypto_ng_amm_deployer(): + return boa.load_partial( + "contracts/amms/twocryptong/CurveTwocryptoOptimized.vy" + ) + + +@pytest.fixture(scope="module") +def twocrypto_ng_amm_implementation(deployer, twocrypto_ng_amm_deployer): + with boa.env.prank(deployer): + return twocrypto_ng_amm_deployer.deploy_as_blueprint() + + +@pytest.fixture(scope="module") +def twocrypto_ng_views_contract(deployer): + with boa.env.prank(deployer): + return boa.load( + "contracts/amms/twocryptong/CurveCryptoViews2Optimized.vy" + ) + + +@pytest.fixture(scope="module") +def twocrypto_ng_factory( + deployer, + ng_fee_receiver, + ng_owner, + twocrypto_ng_amm_implementation, + twocrypto_ng_math_contract, + twocrypto_ng_views_contract, +): + with boa.env.prank(deployer): + factory = boa.load( + "contracts/amms/twocryptong/CurveTwocryptoFactory.vy" + ) + factory.initialise_ownership(ng_fee_receiver, ng_owner) + + with boa.env.prank(ng_owner): + factory.set_pool_implementation(twocrypto_ng_amm_implementation, 0) + factory.set_views_implementation(twocrypto_ng_views_contract) + factory.set_math_implementation(twocrypto_ng_math_contract) + + return factory + + +# ---- Tricrypto NG ---- # + + +@pytest.fixture(scope="module") +def tricrypto_ng_math_contract(deployer): + with boa.env.prank(deployer): + return boa.load( + "contracts/amms/tricryptong/CurveCryptoMathOptimized3.vy" + ) + + +@pytest.fixture(scope="module") +def tricrypto_ng_amm_deployer(): + return boa.load_partial( + "contracts/amms/tricryptong/CurveTricryptoOptimized.vy" + ) + + +@pytest.fixture(scope="module") +def tricrypto_ng_amm_implementation(deployer, tricrypto_ng_amm_deployer): + with boa.env.prank(deployer): + return tricrypto_ng_amm_deployer.deploy_as_blueprint() + + +@pytest.fixture(scope="module") +def tricrypto_ng_views_contract(deployer): + with boa.env.prank(deployer): + return boa.load( + "contracts/amms/tricryptong/CurveCryptoViews3Optimized.vy" + ) + + +@pytest.fixture(scope="module") +def tricrypto_ng_factory( + deployer, + ng_fee_receiver, + ng_owner, + tricrypto_ng_amm_implementation, + tricrypto_ng_math_contract, + tricrypto_ng_views_contract, +): + with boa.env.prank(deployer): + factory = boa.load( + "contracts/amms/tricryptong/CurveTricryptoFactory.vy", + ng_fee_receiver, + ng_owner, + ) + + with boa.env.prank(ng_owner): + factory.set_pool_implementation(tricrypto_ng_amm_implementation, 0) + factory.set_views_implementation(tricrypto_ng_views_contract) + factory.set_math_implementation(tricrypto_ng_math_contract) + + return factory diff --git a/tests/fixtures/pools.py b/tests/fixtures/pools.py new file mode 100644 index 0000000..ad2783d --- /dev/null +++ b/tests/fixtures/pools.py @@ -0,0 +1,128 @@ +import boa +import pytest +from eth.constants import ZERO_ADDRESS + + +@pytest.fixture() +def stableswap_ng_pool( + stableswap_ng_deployer, deployer, stableswap_ng_factory, base_pool_coins +): + pool_size = len(base_pool_coins) + A = 2000 + fee = 1000000 + method_ids = [b""] * pool_size + oracles = [ZERO_ADDRESS] * pool_size + A = 500 + fee = 4000000 + offpeg_fee_multiplier = 20000000000 + + with boa.env.prank(deployer): + ma_exp_time = 866 + implementation_idx = 0 + asset_types = [t.asset_type() for t in base_pool_coins] + coins = [t.address for t in base_pool_coins] + + pool = stableswap_ng_factory.deploy_plain_pool( + "ssng", + "ssng", + coins, + A, + fee, + offpeg_fee_multiplier, + ma_exp_time, + implementation_idx, + asset_types, + method_ids, + oracles, + ) + return stableswap_ng_deployer.at(pool) + + +@pytest.fixture() +def base_ng_pool(stableswap_ng_pool): + return stableswap_ng_pool + + +@pytest.fixture() +def stableswap_ng_metapool( + stableswap_ng_factory, stableswap_ng_meta_deployer, base_ng_pool, sdai +): + A = 2000 + fee = 1000000 + method_id = bytes(b"") + oracle = ZERO_ADDRESS + asset_type = 3 + A = 500 + fee = 4000000 + offpeg_fee_multiplier = 20000000000 + + pool = stableswap_ng_factory.deploy_metapool( + base_ng_pool.address, + "ssngmeta", + "ssngmeta", + sdai.address, + A, + fee, + offpeg_fee_multiplier, + 866, + 0, + asset_type, + method_id, + oracle, + ) + return stableswap_ng_meta_deployer.at(pool) + + +@pytest.fixture(scope="module") +def tricrypto_ng_pool( + tricrypto_ng_factory, + tricrypto_ng_amm_deployer, + tricrypto_ng_pool_coins, + deployer, +): + with boa.env.prank(deployer): + swap = tricrypto_ng_factory.deploy_pool( + "Curve.fi crvUSDC-BTC-ETH", + "tricryptoCRV", + [coin.address for coin in tricrypto_ng_pool_coins], + ZERO_ADDRESS, + 0, + 135 * 3**3 * 10000, + int(7e-5 * 1e18), + int(4e-4 * 1e10), + int(4e-3 * 1e10), + int(0.01 * 1e18), + 2 * 10**12, + int(0.0015 * 1e18), + 866, + [47500 * 10**18, 1500 * 10**18], + ) + + return tricrypto_ng_amm_deployer.at(swap) + + +@pytest.fixture() +def twocrypto_ng_pool( + twocrypto_ng_factory, + twocrypto_ng_amm_deployer, + twocrypto_ng_coins, + deployer, +): + with boa.env.prank(deployer): + swap = twocrypto_ng_factory.deploy_pool( + "Curve.fi crvUSD<>WETH", + "crvUSDWETH", + [coin.address for coin in twocrypto_ng_coins], + 0, + 400000, + 145000000000000, + 26000000, + 45000000, + 230000000000000, + 2000000000000, + 146000000000000, + 866, + 1500 * 10**18, + ) + + return twocrypto_ng_amm_deployer.at(swap) diff --git a/tests/fixtures/tokens.py b/tests/fixtures/tokens.py new file mode 100644 index 0000000..cec1228 --- /dev/null +++ b/tests/fixtures/tokens.py @@ -0,0 +1,57 @@ +import boa +import pytest + + +@pytest.fixture(scope="session") +def erc20_deployer(): + return boa.load_partial("contracts/mocks/ERC20.vy") + + +@pytest.fixture(scope="session") +def erc4626_deployer(): + return boa.load_partial("contracts/mocks/ERC4626.vy") + + +@pytest.fixture() +def usdc(erc20_deployer): + return erc20_deployer.deploy("USDC", "USDC", 6) + + +@pytest.fixture() +def usdt(erc20_deployer): + return erc20_deployer.deploy("USDT", "USDT", 6) + + +@pytest.fixture() +def crvusd(erc20_deployer): + return erc20_deployer.deploy("crvUSD", "crvUSD", 18) + + +@pytest.fixture() +def sdai(erc4626): + return erc4626_deployer.deploy("sDAI", "sDAI", 18, 1056565529321686702) + + +@pytest.fixture() +def base_pool_coins(usdc, usdt, crvusd): + return [usdc, usdt, crvusd] + + +@pytest.fixture() +def tricrypto_ng_pool_coins(crvusd, wbtc, weth): + return [crvusd, wbtc, weth] + + +@pytest.fixture() +def twocrypto_ng_pool_coins(crvusd, weth): + return [crvusd, weth] + + +@pytest.fixture() +def weth(erc20_deployer): + return erc20_deployer.deploy("WETH", "WETH", 18) + + +@pytest.fixture() +def wbtc(erc20_deployer): + return erc20_deployer.deploy("WBTC", "WBTC", 8) diff --git a/tests/mainnet/metaregistry/api/test_find_pool_for_coins.py b/tests/mainnet/metaregistry/api/test_find_pool_for_coins.py index 81b77a6..1715c2f 100644 --- a/tests/mainnet/metaregistry/api/test_find_pool_for_coins.py +++ b/tests/mainnet/metaregistry/api/test_find_pool_for_coins.py @@ -2,8 +2,7 @@ from os import environ import pytest - -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS # NOTE: This is the most important method in the metaregistry contract since it will be used # by integrators to find pools for coin pairs. It finds pools even if the coin pair is not diff --git a/tests/mainnet/metaregistry/api/test_get_admin_balances.py b/tests/mainnet/metaregistry/api/test_get_admin_balances.py index b16df4a..0395b4a 100644 --- a/tests/mainnet/metaregistry/api/test_get_admin_balances.py +++ b/tests/mainnet/metaregistry/api/test_get_admin_balances.py @@ -5,7 +5,7 @@ from boa import BoaError from eth.codecs.abi.exceptions import DecodeError as ABIDecodeError -from scripts.deployment_utils import get_deployed_contract +from scripts.utils import get_deployed_contract from tests.utils import assert_decode_error, assert_negative_coin_balance diff --git a/tests/mainnet/metaregistry/api/test_get_base_pool.py b/tests/mainnet/metaregistry/api/test_get_base_pool.py index f78e1b8..278a8a3 100644 --- a/tests/mainnet/metaregistry/api/test_get_base_pool.py +++ b/tests/mainnet/metaregistry/api/test_get_base_pool.py @@ -1,4 +1,4 @@ -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS def test_all(populated_metaregistry, populated_base_pool_registry, pool): diff --git a/tests/mainnet/metaregistry/api/test_get_coin_indices.py b/tests/mainnet/metaregistry/api/test_get_coin_indices.py index 287b422..73c41bd 100644 --- a/tests/mainnet/metaregistry/api/test_get_coin_indices.py +++ b/tests/mainnet/metaregistry/api/test_get_coin_indices.py @@ -2,8 +2,7 @@ import warnings import pytest - -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS def _reject_pools_with_one_coin(metaregistry, pool): diff --git a/tests/mainnet/metaregistry/api/test_get_gauge.py b/tests/mainnet/metaregistry/api/test_get_gauge.py index 9ea6a7f..a06bf1c 100644 --- a/tests/mainnet/metaregistry/api/test_get_gauge.py +++ b/tests/mainnet/metaregistry/api/test_get_gauge.py @@ -1,6 +1,5 @@ from boa import BoaError - -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS def _is_dao_onboarded_gauge(_gauge, gauge_controller, liquidity_gauge): diff --git a/tests/mainnet/metaregistry/api/test_get_n_coins.py b/tests/mainnet/metaregistry/api/test_get_n_coins.py index 20f24d2..91fd545 100644 --- a/tests/mainnet/metaregistry/api/test_get_n_coins.py +++ b/tests/mainnet/metaregistry/api/test_get_n_coins.py @@ -1,4 +1,4 @@ -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS def _get_n_coins_for_pool(registry, pool): diff --git a/tests/mainnet/metaregistry/api/test_get_n_underlying_coins.py b/tests/mainnet/metaregistry/api/test_get_n_underlying_coins.py index 16ad66d..ff2caca 100644 --- a/tests/mainnet/metaregistry/api/test_get_n_underlying_coins.py +++ b/tests/mainnet/metaregistry/api/test_get_n_underlying_coins.py @@ -1,4 +1,4 @@ -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS def _get_num_coins(registry, pool, base_pool_registry): diff --git a/tests/mainnet/metaregistry/api/test_get_pool_name.py b/tests/mainnet/metaregistry/api/test_get_pool_name.py index a17612a..fafbfc4 100644 --- a/tests/mainnet/metaregistry/api/test_get_pool_name.py +++ b/tests/mainnet/metaregistry/api/test_get_pool_name.py @@ -1,4 +1,5 @@ -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS + from scripts.deployment_utils import get_deployed_contract diff --git a/tests/mainnet/metaregistry/api/test_get_underlying_balances.py b/tests/mainnet/metaregistry/api/test_get_underlying_balances.py index 8296927..3731299 100644 --- a/tests/mainnet/metaregistry/api/test_get_underlying_balances.py +++ b/tests/mainnet/metaregistry/api/test_get_underlying_balances.py @@ -2,9 +2,9 @@ import pytest from boa import BoaError +from eth.constants import ZERO_ADDRESS -from scripts.constants import ZERO_ADDRESS -from scripts.deployment_utils import get_deployed_contract +from scripts.utils import get_deployed_contract from tests.utils import assert_negative_coin_balance EXCEPTION_POOLS = ["0x79a8C46DeA5aDa233ABaFFD40F3A0A2B1e5A4F27"] diff --git a/tests/mainnet/metaregistry/api/test_get_underlying_coins.py b/tests/mainnet/metaregistry/api/test_get_underlying_coins.py index 91c990a..214499c 100644 --- a/tests/mainnet/metaregistry/api/test_get_underlying_coins.py +++ b/tests/mainnet/metaregistry/api/test_get_underlying_coins.py @@ -2,8 +2,7 @@ import boa from boa import BoaError - -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS def _get_underlying_coins( diff --git a/tests/mainnet/metaregistry/api/test_get_underlying_decimals.py b/tests/mainnet/metaregistry/api/test_get_underlying_decimals.py index a4d9236..117179f 100644 --- a/tests/mainnet/metaregistry/api/test_get_underlying_decimals.py +++ b/tests/mainnet/metaregistry/api/test_get_underlying_decimals.py @@ -1,8 +1,8 @@ import pytest from boa import BoaError +from eth.constants import ZERO_ADDRESS -from scripts.constants import ZERO_ADDRESS -from scripts.deployment_utils import get_deployed_contract +from scripts.utils import get_deployed_contract EXCEPTIONS = { # eth: ankreth pool returns [18, 0] when it should return: diff --git a/tests/mainnet/metaregistry/api/test_get_virtual_price.py b/tests/mainnet/metaregistry/api/test_get_virtual_price.py index e44aeee..33fb6ec 100644 --- a/tests/mainnet/metaregistry/api/test_get_virtual_price.py +++ b/tests/mainnet/metaregistry/api/test_get_virtual_price.py @@ -4,9 +4,9 @@ import pytest from boa import BoaError from eth.codecs.abi.exceptions import DecodeError as ABIDecodeError +from eth.constants import ZERO_ADDRESS -from scripts.constants import ZERO_ADDRESS -from scripts.deployment_utils import get_deployed_contract +from scripts.utils import get_deployed_contract from tests.utils import assert_decode_error, assert_negative_coin_balance # ---- sanity checks since vprice getters can revert for specific pools states ---- diff --git a/tests/mainnet/metaregistry/api/test_pool_is_metapool.py b/tests/mainnet/metaregistry/api/test_pool_is_metapool.py index 3375ba8..08a8cf6 100644 --- a/tests/mainnet/metaregistry/api/test_pool_is_metapool.py +++ b/tests/mainnet/metaregistry/api/test_pool_is_metapool.py @@ -1,4 +1,4 @@ -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS def test_stable_registry_pools( diff --git a/tests/mainnet/metaregistry/test_base_registry_tracking.py b/tests/mainnet/metaregistry/test_base_registry_tracking.py index ca776b3..e5fe376 100644 --- a/tests/mainnet/metaregistry/test_base_registry_tracking.py +++ b/tests/mainnet/metaregistry/test_base_registry_tracking.py @@ -1,4 +1,4 @@ -from scripts.constants import ZERO_ADDRESS +from eth.constants import ZERO_ADDRESS def test_new_crypto_factory_pool( diff --git a/tests/mainnet/registries/test_add_remove_basepool.py b/tests/mainnet/registries/test_add_remove_basepool.py index f96f9ff..c645631 100644 --- a/tests/mainnet/registries/test_add_remove_basepool.py +++ b/tests/mainnet/registries/test_add_remove_basepool.py @@ -1,6 +1,6 @@ import boa +from eth.constants import ZERO_ADDRESS -from scripts.constants import ZERO_ADDRESS from tests.utils import deploy_contract diff --git a/tests/mainnet/registries/test_add_remove_metapool.py b/tests/mainnet/registries/test_add_remove_metapool.py index fcc1918..568ddca 100644 --- a/tests/mainnet/registries/test_add_remove_metapool.py +++ b/tests/mainnet/registries/test_add_remove_metapool.py @@ -1,6 +1,6 @@ import boa +from eth.constants import ZERO_ADDRESS -from scripts.constants import ZERO_ADDRESS from tests.utils import deploy_contract diff --git a/tests/mainnet/registries/test_add_remove_pool.py b/tests/mainnet/registries/test_add_remove_pool.py index abac879..59707fa 100644 --- a/tests/mainnet/registries/test_add_remove_pool.py +++ b/tests/mainnet/registries/test_add_remove_pool.py @@ -1,6 +1,6 @@ import boa +from eth.constants import ZERO_ADDRESS -from scripts.constants import ZERO_ADDRESS from tests.utils import deploy_contract diff --git a/tests/unitary/conftest.py b/tests/unitary/conftest.py new file mode 100644 index 0000000..f6138f3 --- /dev/null +++ b/tests/unitary/conftest.py @@ -0,0 +1,94 @@ +import boa +import pytest + +from tests.utils import deploy_contract + +pytest_plugins = [ + "tests.fixtures.accounts", + "tests.fixtures.constants", + "tests.fixtures.deployments", + "tests.fixtures.functions", +] + + +@pytest.fixture(scope="module") +def base_pool_registry(ng_owner): + return deploy_contract( + "BasePoolRegistry", sender=ng_owner, directory="registries" + ) + + +@pytest.fixture(scope="module") +def populated_base_pool_registry(base_pool_registry, ng_owner, base_pools): + with boa.env.sender(ng_owner): + for data in base_pools.values(): + base_pool_registry.add_base_pool( + data["pool"], + data["lp_token"], + data["num_coins"], + data["is_legacy"], + data["is_lending"], + data["is_v2"], + ) + return base_pool_registry + + +@pytest.fixture(scope="module") +def stable_ng_factory_handler( + populated_base_pool_registry, stableswap_ng_factory, ng_owner +): + with boa.env.prank(ng_owner): + return boa.load( + "contracts/mainnet/registry_handlers/ng/CurveStableSwapFactoryNGHandler.vy", + stableswap_ng_factory.address, + populated_base_pool_registry.address, + ) + + +@pytest.fixture(scope="module") +def twocrypto_ng_factory_handler(twocrypto_ng_factory, ng_owner): + with boa.env.prank(ng_owner): + return boa.load( + "contracts/mainnet/registry_handlers/ng/CurveTwocryptoFactoryHandler.vy", + twocrypto_ng_factory.address, + ) + + +@pytest.fixture(scope="module") +def tricrypto_ng_factory_handler(tricrypto_ng_factory, ng_owner): + with boa.env.prank(ng_owner): + return boa.load( + "contracts/mainnet/registry_handlers/ng/CurveTricryptoFactoryHandler.vy", + tricrypto_ng_factory.address, + ) + + +@pytest.fixture(scope="module") +def handlers( + stable_ng_factory_handler, + twocrypto_ng_factory_handler, + tricrypto_ng_factory_handler, +): + return [ + stable_ng_factory_handler, + twocrypto_ng_factory_handler, + tricrypto_ng_factory_handler, + ] + + +@pytest.fixture(scope="module") +def registries(handlers): + registries = [] + for handler in handlers: + registries.append(handler.base_registry()) + return registries + + +@pytest.fixture(scope="module") +def metaregistry(handlers, ng_owner): + metaregistry_contract = deploy_contract("MetaRegistry", sender=ng_owner) + for handler in handlers: + metaregistry_contract.add_registry_handler( + handler.address, sender=ng_owner + ) + return metaregistry_contract diff --git a/tests/unitary/mainnet/metaregistry/test_find_pools_for_coins.py b/tests/unitary/mainnet/metaregistry/test_find_pools_for_coins.py new file mode 100644 index 0000000..1715c2f --- /dev/null +++ b/tests/unitary/mainnet/metaregistry/test_find_pools_for_coins.py @@ -0,0 +1,58 @@ +from itertools import combinations +from os import environ + +import pytest +from eth.constants import ZERO_ADDRESS + +# NOTE: This is the most important method in the metaregistry contract since it will be used +# by integrators to find pools for coin pairs. It finds pools even if the coin pair is not +# a direct coin pair, but has a path through a metapool. + + +def _get_all_combinations(metaregistry, pool): + pool_coins = [ + coin for coin in metaregistry.get_coins(pool) if coin != ZERO_ADDRESS + ] + all_combinations = list(combinations(pool_coins, 2)) + first_coin = pool_coins[0] + + # there exist some pools with an LP token as the first coin, that's incorrect + # example: 0xf5d5305790c1af08e9df44b30a1afe56ccda72df + lp_token_pool = metaregistry.get_pool_from_lp_token(first_coin) + is_first_coin_lp_token = lp_token_pool and lp_token_pool != ZERO_ADDRESS + + if metaregistry.is_meta(pool) and not is_first_coin_lp_token: + underlying_coins = [ + coin + for coin in metaregistry.get_underlying_coins(pool) + if coin != ZERO_ADDRESS + ] + all_combinations += [ + (first_coin, coin) + for coin in underlying_coins + if first_coin != coin + ] + + return all_combinations + + +@pytest.mark.skipif( + condition=environ.get("TEST_ALL") == "False", + reason="This test is too slow, don't run it locally every time.", +) +def test_all(populated_metaregistry, pool): + all_combinations = _get_all_combinations(populated_metaregistry, pool) + for coin1, coin2 in all_combinations: + pools_containing_pair = populated_metaregistry.find_pools_for_coins( + coin1, coin2 + ) + assert pool in pools_containing_pair, ( + f"Cannot find pool {pool} for coin combination {coin1} and {coin2}. " + f"Pools found {pools_containing_pair}" + ) + + # test with specified index + assert pools_containing_pair == [ + populated_metaregistry.find_pool_for_coins(coin1, coin2, i) + for i in range(len(pools_containing_pair)) + ] diff --git a/tests/utils.py b/tests/utils.py index f8d6a55..d26117a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,8 +6,8 @@ from eth.codecs.abi.exceptions import DecodeError as ABIDecodeError from eth_account.signers.local import LocalAccount -from scripts.constants import BASE_DIR, ZERO_ADDRESS from scripts.deployment_utils import get_deployed_contract +from scripts.utils.constants import BASE_DIR, ZERO_ADDRESS def get_contract_pools(contract_name: str, address: str) -> list[str]: