Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add quoter #27

Merged
merged 4 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions contracts/RateProvider.vy
Original file line number Diff line number Diff line change
@@ -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
84 changes: 84 additions & 0 deletions scripts/deploy_rate_provider.py
Original file line number Diff line number Diff line change
@@ -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)
Loading