diff --git a/contracts/RateProvider.vy b/contracts/RateProvider.vy new file mode 100644 index 0000000..ba16d16 --- /dev/null +++ b/contracts/RateProvider.vy @@ -0,0 +1,231 @@ +# pragma version 0.3.10 +# pragma evm-version paris +""" +@title CurveRateProvider +@custom:version 1.0.0 +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +@notice Provides quotes for coin pairs, iff coin pair is in a Curve AMM that the Metaregistry recognises. +""" + +version: public(constant(String[8])) = "1.0.0" + +from vyper.interfaces import ERC20Detailed + +MAX_COINS: constant(uint256) = 8 +MAX_QUOTES: constant(uint256) = 100 + +struct Quote: + source_token_index: uint256 + dest_token_index: uint256 + is_underlying: bool + amount_out: uint256 + pool: address + source_token_pool_balance: uint256 + dest_token_pool_balance: uint256 + pool_type: uint8 # 0 for stableswap, 1 for cryptoswap, 2 for LLAMMA. + + +# Interfaces + +interface AddressProvider: + def get_address(id: uint256) -> address: view + +interface Metaregistry: + def find_pools_for_coins(source_coin: address, destination_coin: address) -> DynArray[address, 1000]: view + def get_coin_indices(_pool: address, _from: address, _to: address) -> (int128, int128, bool): view + def get_underlying_balances(_pool: address) -> uint256[MAX_COINS]: view + + +ADDRESS_PROVIDER: public(immutable(AddressProvider)) +METAREGISTRY_ID: constant(uint256) = 7 +STABLESWAP_META_ABI: constant(String[64]) = "get_dy_underlying(int128,int128,uint256)" +STABLESWAP_ABI: constant(String[64]) = "get_dy(int128,int128,uint256)" +CRYPTOSWAP_ABI: constant(String[64]) = "get_dy(uint256,uint256,uint256)" + +@external +def __init__(address_provider: address): + ADDRESS_PROVIDER = AddressProvider(address_provider) + + +@external +@view +def get_quotes(source_token: address, destination_token: address, amount_in: uint256) -> DynArray[Quote, MAX_QUOTES]: + return self._get_quotes(source_token, destination_token, amount_in) + + +@external +@view +def get_aggregated_rate(source_token: address, destination_token: address) -> uint256: + + amount_in: uint256 = 10**convert(ERC20Detailed(source_token).decimals(), uint256) + quotes: DynArray[Quote, MAX_QUOTES] = self._get_quotes(source_token, destination_token, amount_in) + + return self.weighted_average_quote( + convert(ERC20Detailed(source_token).decimals(), uint256), + convert(ERC20Detailed(destination_token).decimals(), uint256), + quotes, + ) + + +@internal +@pure +def weighted_average_quote( + source_token_decimals: uint256, + dest_token_decimals: uint256, + quotes: DynArray[Quote, MAX_QUOTES] +) -> uint256: + + num_quotes: uint256 = len(quotes) + + # Calculate total balance with normalization + total_balance: uint256 = 0 + for i in range(num_quotes, bound=MAX_QUOTES): + source_balance_normalized: uint256 = quotes[i].source_token_pool_balance * 10**(18 - source_token_decimals) + dest_balance_normalized: uint256 = quotes[i].dest_token_pool_balance * 10**(18 - dest_token_decimals) + total_balance += source_balance_normalized + dest_balance_normalized + + + # Calculate weighted sum with normalization + weighted_avg: uint256 = 0 + for i in range(num_quotes, bound=MAX_QUOTES): + source_balance_normalized: uint256 = quotes[i].source_token_pool_balance * 10**(18 - source_token_decimals) + dest_balance_normalized: uint256 = quotes[i].dest_token_pool_balance * 10**(18 - dest_token_decimals) + pool_balance_normalized: uint256 = source_balance_normalized + dest_balance_normalized + weight: uint256 = (pool_balance_normalized * 10**18) / total_balance # Use 18 decimal places for precision + weighted_avg += weight * quotes[i].amount_out / 10**18 + + return weighted_avg + + +@internal +@view +def _get_quotes(source_token: address, destination_token: address, amount_in: uint256) -> DynArray[Quote, MAX_QUOTES]: + + quotes: DynArray[Quote, MAX_QUOTES] = [] + metaregistry: Metaregistry = Metaregistry(ADDRESS_PROVIDER.get_address(METAREGISTRY_ID)) + pools: DynArray[address, 1000] = metaregistry.find_pools_for_coins(source_token, destination_token) + + if len(pools) == 0: + return quotes + + # get pool types for each pool + for pool in pools: + + # is it a stableswap pool? are the coin pairs part of a metapool? + pool_type: uint8 = self._get_pool_type(pool, metaregistry) + + # get coin indices + i: int128 = 0 + j: int128 = 0 + is_underlying: bool = False + (i, j, is_underlying) = metaregistry.get_coin_indices(pool, source_token, destination_token) + + # get balances + balances: uint256[MAX_COINS] = metaregistry.get_underlying_balances(pool) + dyn_balances: DynArray[uint256, MAX_COINS] = [] + for bal in balances: + if bal > 0: + dyn_balances.append(bal) + + # skip if pool is too small + if 0 in dyn_balances: + continue + + # do a get_dy call and only save quote if call does not bork; use correct abi (in128 vs uint256) + quote: uint256 = self._get_pool_quote(i, j, amount_in, pool, pool_type, is_underlying) + + # check if get_dy works and if so, append quote to dynarray + if quote > 0 and len(quotes) < MAX_QUOTES: + quotes.append( + Quote( + { + source_token_index: convert(i, uint256), + dest_token_index: convert(j, uint256), + is_underlying: is_underlying, + amount_out: quote, + pool: pool, + source_token_pool_balance: balances[i], + dest_token_pool_balance: balances[j], + pool_type: pool_type + } + ) + ) + + return quotes + + +@internal +@view +def _get_pool_type(pool: address, metaregistry: Metaregistry) -> uint8: + + # 0 for stableswap, 1 for cryptoswap, 2 for LLAMMA. + + success: bool = False + response: Bytes[32] = b"" + + # check if cryptoswap + success, response = raw_call( + pool, + method_id("allowed_extra_profit()"), + max_outsize=32, + revert_on_failure=False, + is_static_call=True + ) + if success: + return 1 + + # check if llamma + success, response = raw_call( + pool, + method_id("get_rate_mul()"), + max_outsize=32, + revert_on_failure=False, + is_static_call=True + ) + if success: + return 2 + + return 0 + + +@internal +@view +def _get_pool_quote( + i: int128, + j: int128, + amount_in: uint256, + pool: address, + pool_type: uint8, + is_underlying: bool +) -> uint256: + + success: bool = False + response: Bytes[32] = b"" + method_abi: Bytes[4] = b"" + + # choose the right abi: + if pool_type == 0 and is_underlying: + method_abi = method_id(STABLESWAP_META_ABI) + elif pool_type == 0 and not is_underlying: + method_abi = method_id(STABLESWAP_ABI) + else: + method_abi = method_id(CRYPTOSWAP_ABI) + + success, response = raw_call( + pool, + concat( + method_abi, + convert(i, bytes32), + convert(j, bytes32), + convert(amount_in, bytes32), + ), + max_outsize=32, + revert_on_failure=False, + is_static_call=True + ) + + if success: + return convert(response, uint256) + + return 0 diff --git a/scripts/deploy_rate_provider.py b/scripts/deploy_rate_provider.py new file mode 100644 index 0000000..4758560 --- /dev/null +++ b/scripts/deploy_rate_provider.py @@ -0,0 +1,84 @@ +# 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.legacy_base_pools import base_pools as BASE_POOLS +from scripts.utils.constants import FIDDY_DEPLOYER + +console = rich_console.Console() + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +ADDRESS_PROVIDER = ( + "0x5ffe7FB82894076ECB99A30D6A32e969e6e35E98" # gets replaced for zksync +) + + +def main(network, fork, url): + if network == "zksync": + if not fork: + boa_zksync.set_zksync_env(url) + console.log("Prodmode on zksync Era ...") + else: + boa_zksync.set_zksync_fork(url) + console.log("Forkmode on zksync Era ...") + + boa.env.set_eoa(Account.from_key(os.environ["FIDDYDEPLOYER"])) + + else: + if fork: + boa.env.fork(url) + console.log("Forkmode ...") + boa.env.eoa = FIDDY_DEPLOYER # set eoa address here + else: + console.log("Prodmode ...") + boa.set_env(NetworkEnv(url)) + boa.env.add_account(Account.from_key(os.environ["FIDDYDEPLOYER"])) + + address_provider = boa.load_partial("contracts/AddressProviderNG.vy").at( + ADDRESS_PROVIDER + ) + + console.log("Deploying rate provider ...") + rate_provider = boa.load( + "contracts/RateProvider.vy", address_provider.address + ) + + console.log("Adding rate provider to address provider") + if address_provider.get_address(18) == ZERO_ADDRESS: + address_provider.add_new_id( + 18, rate_provider.address, "Spot Rate Provider" + ) + elif address_provider.get_address(18) != rate_provider.address: + address_provider.update_address(18, rate_provider.address) + + +if __name__ == "__main__": + network = "zksync" + url = "" + fork = False + + if network == "zksync": + import boa_zksync + + network_url = "https://mainnet.era.zksync.io" + ADDRESS_PROVIDER = "0x54A5a69e17Aa6eB89d77aa3828E38C9Eb4fF263D" + elif network == "fraxtal": + network_url = "https://rpc.frax.com" + elif network == "kava": + network_url = "https://rpc.ankr.com/kava_evm" + elif network == "xlayer": + network_url = "https://xlayerrpc.okx.com" + elif network == "mantle": + network_url = "https://rpc.mantle.xyz" + else: + network_url = fetch_url(network) + + main(network, fork, network_url)