Source code for eth_defi.uniswap_v2.fees

"""Uniswap v2 token price, fee and price impact calculations.

- To get a price in Uniswap v2 pool in human-readable format see
  :py:func:`estimate_sell_price` and :py:func:`estimate_buy_price`.

`Mostly lifted from Uniswap-v2-py MIT licensed by Asynctomatic <https://github.com/nosofa/uniswap-v2-py>`_.
"""

from decimal import Decimal
from typing import Optional

from eth_typing import HexAddress
from web3.contract import Contract
from web3.exceptions import BadFunctionCallOutput

from eth_defi.token import fetch_erc20_details
from eth_defi.uniswap_v2.deployment import INIT_CODE_HASH_MISSING, UniswapV2Deployment


[docs]class BadReserves(Exception): pass
[docs]class UniswapV2FeeCalculator: """A helper class to estimate Uniswap fees."""
[docs] def __init__(self, uniswap_v2: UniswapV2Deployment): self.deployment = uniswap_v2
# Liften from uniswap-v2-py by Asynctomatic
[docs] def get_reserves(self, token_a: HexAddress, token_b: HexAddress) -> tuple[int]: """Gets the reserves of token_0 and token_1 used to price trades and distribute liquidity as well as the timestamp of the last block during which an interaction occurred for the pair. :param pair: Address of the pair. :return: - reserve_0 - Amount of token_0 in the contract. - reserve_1 - Amount of token_1 in the contract. - liquidity - Unix timestamp of the block containing the last pair interaction. """ assert self.deployment.init_code_hash is not None, "Init hash not set" assert self.deployment.init_code_hash != INIT_CODE_HASH_MISSING, "You need to set init hash to use get_reserves()" assert token_a.startswith("0x") assert token_b.startswith("0x") # (token0, token1) = sort_tokens(token_a, token_b) pair_address, token0, token1 = self.deployment.pair_for(token_a, token_b) pair_contract = self.deployment.PairContract(pair_address) try: reserve = pair_contract.functions.getReserves().call() except BadFunctionCallOutput as e: raise BadReserves(f"Could not get reserves, bad pair contract {pair_address}, init hash {self.deployment.init_code_hash}, token_a {token_a}, token_b {token_b}?") from e return reserve if token0 == token_a else [reserve[1], reserve[0], reserve[2]]
[docs] def get_amount_out( self, amount_in: int, path: list[HexAddress], *, fee: int = 30, slippage: float = 0, ) -> int: """Get how much token we are going to receive. :param amount_in: Amount of input asset. :param path: List of token addresses how to route the trade :param fee: Trading fee express in bps, default = 30 bps (0.3%) :param slippage: Slippage express in bps :return: """ assert len(path) >= 2 assert slippage >= 0 amounts = [amount_in] current_amount = amount_in pairs = list(zip(path, path[1:])) for p0, p1 in pairs: r = self.get_reserves(p0, p1) current_amount = self.get_amount_out_from_reserves(current_amount, r[0], r[1], fee=fee) amounts.append(current_amount) amount_out = amounts[-1] return int(amount_out * 10_000 // (10_000 + slippage))
[docs] def get_amount_in( self, amount_out: int, path: list[HexAddress], *, fee: int = 30, slippage: float = 0, ) -> int: """Get how much token we are going to spend. :param amount_out: Amount of output asset. :param path: List of token addresses how to route the trade :param fee: Trading fee express in bps, default = 30 bps (0.3%) :param slippage: Slippage express in bps :return: """ assert len(path) >= 2 assert slippage >= 0 amounts = [amount_out] current_amount = amount_out pairs = reversed(list(zip(path, path[1:]))) for p0, p1 in pairs: r = self.get_reserves(p0, p1) current_amount = self.get_amount_in_from_reserves(current_amount, r[0], r[1], fee=fee) amounts.insert(0, current_amount) amount_in = amounts[0] return int(amount_in * (10_000 + slippage) // 10_000)
[docs] @staticmethod def get_amount_in_from_reserves( amount_out: int, reserve_in: int, reserve_out: int, *, fee: int = 30, ) -> int: """Returns the minimum input asset amount required to buy the given output asset amount (accounting for fees) given reserves. :param amount_out: Amount of output asset. :param reserve_in: Reserve of input asset in the pair contract. :param reserve_out: Reserve of output asset in the pair contract. :param fee: Trading fee express in bps, default = 30 bps (0.3%) :return: Required amount of input asset. """ assert amount_out > 0 assert reserve_in > 0 and reserve_out > 0 numerator = reserve_in * amount_out * 10_000 denominator = (reserve_out - amount_out) * (10_000 - fee) return numerator // denominator + 1
[docs] @staticmethod def get_amount_out_from_reserves( amount_in: int, reserve_in: int, reserve_out: int, *, fee: int = 30, ) -> int: """Given an input asset amount, returns the maximum output amount of the other asset (accounting for fees) given reserves. :param amount_in: Amount of input asset. :param reserve_in: Reserve of input asset in the pair contract. :param reserve_out: Reserve of output asset in the pair contract. :param fee: Trading fee express in bps, default = 30 bps (0.3%) :return: Maximum amount of output asset. """ assert amount_in > 0 assert reserve_in > 0 and reserve_out > 0 amount_in_with_fee = amount_in * (10_000 - fee) numerator = amount_in_with_fee * reserve_out denominator = reserve_in * 10_000 + amount_in_with_fee return numerator // denominator
[docs]def estimate_buy_quantity( uniswap: UniswapV2Deployment, base_token: Contract, quote_token: Contract, quantity: int, *, fee: int = 30, slippage: float = 0, ) -> int: """Estimate how many tokens we are going to receive when doing a buy. Good for doing a price impact calculations. Calls the on-chain contract to get the current liquidity and estimates the the price based on it. Example: .. code-block:: python # Estimate how much ETH we will receive for 500 USDC. # In this case the pool ETH price is $1700 so this should be below ~1/4 of ETH amount_eth = estimate_buy_quantity( uniswap_v2, weth, usdc, 500*10**18, ) assert amount_eth / 1e18 == pytest.approx(0.28488156127668085) :param quantity: How much of the quote token we have to use :param uniswap: Uniswap v2 deployment :param base_token: Base token of the trading pair :param quote_token: Quote token of the trading pair :param fee: Trading fee express in bps, default = 30 bps (0.3%) :param slippage: Slippage express in bps :return: Expected base token to receive """ fee_helper = UniswapV2FeeCalculator(uniswap) path = [quote_token.address, base_token.address] return fee_helper.get_amount_out(quantity, path, fee=fee, slippage=slippage)
[docs]def estimate_buy_price( uniswap: UniswapV2Deployment, base_token: Contract, quote_token: Contract, quantity: int, *, fee: int = 30, slippage: float = 0, intermediate_token: Optional[Contract] = None, ) -> int: """Estimate how much we are going to need to pay when doing buy. Calls the on-chain contract to get the current liquidity and estimates the the price based on it. Example: .. code-block:: python # Estimate how much ETH we will receive for 500 USDC. # In this case the pool ETH price is $1700 so this should be below ~1/4 of ETH amount_eth = estimate_buy_price( uniswap_v2, weth, usdc, 1*10**18, ) assert amount_eth / 1e18 == pytest.approx(0.28488156127668085) :param uniswap: Uniswap v2 deployment :param base_token: Base token of the trading pair :param quote_token: Quote token of the trading pair :param quantity: How much of the base token we want to buy :param fee: Trading fee express in bps, default = 30 bps (0.3%) :param slippage: Slippage express in bps :return: Expected base token to receive """ fee_helper = UniswapV2FeeCalculator(uniswap) if intermediate_token: path = [quote_token.address, intermediate_token.address, base_token.address] else: path = [quote_token.address, base_token.address] return fee_helper.get_amount_in(quantity, path, fee=fee, slippage=slippage)
[docs]def estimate_sell_price( uniswap: UniswapV2Deployment, base_token: Contract, quote_token: Contract, quantity: int, *, fee: int = 30, slippage: float = 0, intermediate_token: Optional[Contract] = None, ) -> int: """Estimate how much we are going to get paid when doing a sell. Calls the on-chain contract to get the current liquidity and estimates the the price based on it. .. note :: The price of an asset depends on how much you are selling it. More you sell, more there will be price impact. To get a price of an asset, ask for quantity 1 of it: .. code-block:: python # Create the trading pair and add iint(10_000 * amounts[-1] // (10_000 - slippage))nitial liquidity for price 1700 USDC/ETH deploy_trading_pair( web3, deployer, uniswap_v2, weth, usdc, 1_000 * 10**18, # 1000 ETH liquidity 1_700_000 * 10**6, # 1.7M USDC liquidity ) # Estimate the price of selling 1 ETH usdc_per_eth = estimate_sell_price( uniswap_v2, weth, usdc, 1 * 10**18, # 1 ETH ) price_as_usd = usdc_per_eth / 1e6 assert price_as_usd == pytest.approx(1693.2118677678354) :param quantity: How much of the base token we want to sell :param uniswap: Uniswap v2 deployment :param base_token: Base token of the trading pair :param quote_token: Quote token of the trading pair :param fee: Trading fee express in bps, default = 30 bps (0.3%) :param slippage: Slippage express in bps :return: Expected quote token amount to receive """ fee_helper = UniswapV2FeeCalculator(uniswap) if intermediate_token: path = [base_token.address, intermediate_token.address, quote_token.address] else: path = [base_token.address, quote_token.address] return fee_helper.get_amount_out(quantity, path, fee=fee, slippage=slippage)
[docs]def estimate_buy_price_decimals( uniswap: UniswapV2Deployment, base_token_address: HexAddress, quote_token_address: HexAddress, quantity: Decimal, *, fee: int = 30, slippage: float = 0, intermediate_token_address: Optional[HexAddress] = None, ) -> Decimal: """Estimate how much we are going to need to pay when doing buy. Much like :py:func:`estimate_buy_price` with the differences of - Tokens are passed as address instead of contract instance - We use base token quantity units instead of cash - We use decimals instead of raw token amounts Example: .. code-block:: python # Create the trading pair and add initial liquidity deploy_trading_pair( web3, deployer, uniswap_v2, weth, usdc, 1_000 * 10**18, # 1000 ETH liquidity 1_700_000 * 10**18, # 1.7M USDC liquidity ) # Estimate the price of buying 1 ETH usdc_per_eth = estimate_buy_price_decimals( uniswap_v2, weth.address, usdc.address, Decimal(1.0), ) assert usdc_per_eth == pytest.approx(Decimal(1706.82216820632059904)) :param quantity: How much of the base token we want to buy :param uniswap: Uniswap v2 deployment :param base_token: Base token of the trading pair :param quote_token: Quote token of the trading pair :param fee: Trading fee express in bps, default = 30 bps (0.3%) :param slippage: Slippage express in bps :return: Expected quote token amount to receive :raise TokenDetailError: If we have an issue with ERC-20 contracts """ web3 = uniswap.web3 quote = fetch_erc20_details(web3, quote_token_address, raise_on_error=False) quantity_raw = quote.convert_to_raw(quantity) fee_helper = UniswapV2FeeCalculator(uniswap) if intermediate_token_address: path = [quote_token_address, intermediate_token_address, base_token_address] else: path = [quote_token_address, base_token_address] in_raw = fee_helper.get_amount_in(quantity_raw, path, fee=fee, slippage=slippage) return quote.convert_to_decimals(in_raw)
[docs]def estimate_sell_price_decimals( uniswap: UniswapV2Deployment, base_token_address: HexAddress, quote_token_address: HexAddress, quantity: Decimal, *, fee: int = 30, slippage: float = 0, intermediate_token_address: Optional[HexAddress] = None, ) -> Decimal: """Estimate how much we are going to get paid when doing a sell. Much like :py:func:`estimate_sell_price` but in/out is expressed as python Decimal units. Furthermore, no ERC-20 token contract needed ABI, but it is loaded by the function. :param quantity: How much of the base token we want to sell, in token units (will be decimal autoconverted). :param uniswap: Uniswap v2 deployment :param base_token: Base token of the trading pair :param quote_token: Quote token of the trading pair :param fee: Trading fee express in bps, default = 30 bps (0.3%) :param slippage: Slippage express in bps :return: Expected quote token amount to receive in quota tokens (decimal converted). :raise TokenDetailError: If we have an issue with ERC-20 contracts """ web3 = uniswap.web3 base = fetch_erc20_details(web3, base_token_address, raise_on_error=False) quote = fetch_erc20_details(web3, quote_token_address, raise_on_error=False) quantity_raw = base.convert_to_raw(quantity) fee_helper = UniswapV2FeeCalculator(uniswap) if intermediate_token_address: path = [base_token_address, intermediate_token_address, quote_token_address] else: path = [base_token_address, quote_token_address] out_raw = fee_helper.get_amount_out(quantity_raw, path, fee=fee, slippage=slippage) return quote.convert_to_decimals(out_raw)
[docs]def estimate_buy_received_amount_raw( uniswap: UniswapV2Deployment, base_token_address: HexAddress, quote_token_address: HexAddress, quantity_raw: Decimal, *, fee: int = 30, slippage: float = 0, intermediate_token_address: Optional[HexAddress] = None, ) -> int: """Estimate how much we receive for a certain cash amount. Example: .. code-block:: python # Create the trading pair and add initial liquidity deploy_trading_pair( web3, deployer, uniswap_v2, weth, usdc, 1_000 * 10**18, # 1000 ETH liquidity 1_700_000 * 10**18, # 1.7M USDC liquidity ) # Estimate the price of buying 1650 USDC worth of ETH eth_received = estimate_buy_received_amount_raw( uniswap_v2, weth.address, usdc.address, 1650 * 10**18, ) assert eth_received / (10**18) == pytest.approx(0.9667409780905836) # Calculate price of ETH as $ for our purchase price = (1650*10**18) / eth_received assert price == pytest.approx(Decimal(1706.7653460381143)) :param quantity: How much of the base token we want to buy :param uniswap: Uniswap v2 deployment :param base_token: Base token of the trading pair :param quote_token: Quote token of the trading pair :param fee: Trading fee express in bps, default = 30 bps (0.3%) :param slippage: Slippage express in bps :return: Expected quote token amount to receive :raise TokenDetailError: If we have an issue with ERC-20 contracts """ fee_helper = UniswapV2FeeCalculator(uniswap) if intermediate_token_address: path = [quote_token_address, intermediate_token_address, base_token_address] else: path = [quote_token_address, base_token_address] # We will receive equal number of amounts as there are items in the path return fee_helper.get_amount_out(quantity_raw, path, fee=fee, slippage=slippage)
[docs]def estimate_sell_received_amount_raw( uniswap: UniswapV2Deployment, base_token_address: HexAddress, quote_token_address: HexAddress, quantity_raw: Decimal, *, fee: int = 30, slippage: float = 0, intermediate_token_address: Optional[HexAddress] = None, ) -> int: """Estimate how much cash we receive for a certain quantity of tokens sold. Example: .. code-block:: python deploy_trading_pair( web3, deployer, uniswap_v2, weth, usdc, 1_000 * 10**18, # 1000 ETH liquidity 1_700_000 * 10**18, # 1.7M USDC liquidity ) # Sell 50 ETH usdc_received = estimate_sell_received_amount_raw( uniswap_v2, weth.address, usdc.address, 50 * 10**18, ) usdc_received_decimals = usdc_received / 10**18 assert usdc_received_decimals == pytest.approx(80721.05538886508) # Calculate price of ETH as $ for our purchase price = usdc_received / (50*10**18) assert price == pytest.approx(Decimal(1614.4211077773016)) :param quantity: How much of the base token we want to buy :param uniswap: Uniswap v2 deployment :param base_token: Base token of the trading pair :param quote_token: Quote token of the trading pair :param fee: Trading fee express in bps, default = 30 bps (0.3%) :param slippage: Slippage express in bps :return: Expected quote token amount to receive :raise TokenDetailError: If we have an issue with ERC-20 contracts """ fee_helper = UniswapV2FeeCalculator(uniswap) if intermediate_token_address: path = (base_token_address, intermediate_token_address, quote_token_address) else: path = (base_token_address, quote_token_address) return fee_helper.get_amount_out(quantity_raw, path, fee=fee, slippage=slippage)