Skip to content

Commit

Permalink
Restructure price fetching (#421)
Browse files Browse the repository at this point in the history
This PR slightly restructures the price fetching for converting token
amount for rewards.

- Instead of having functions for converting to and from ETH
(`eth_in_token`, `token_in_eth`) we just provide an exchange rate for
conversion between two tokens (`exchange_rate_atoms`). This will make it
easier to add conversion between COW and the native token of the chain,
as well as between XDAI and ETH.
- Instead of using token ids as argument, the main function accepts
addresses. This is mostly to avoid leaking the implementation of the
price fetching (token id on coin paprika) into the rest of the code. It
also avoids a circular dependency in making the reward token part of the
configuration in #412.

The `TokenId` abstraction still feels a bit stange as there is some
overlap with `Token` due to decimals. Supporting USDC seems a bit
artificial since that is only used for testing and checking that
decimals are correctly accounted for.

Tests for price fetching were adapted accordingly. Transfers for the
accounting week starting on 2024-10-29 only differ on the level of
floating point precision.

---------

Co-authored-by: Haris Angelidakis <[email protected]>
  • Loading branch information
fhenneke and harisang authored Nov 10, 2024
1 parent a4e9be6 commit 59cb592
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 65 deletions.
22 changes: 13 additions & 9 deletions src/fetch/payouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
from dune_client.types import Address
from pandas import DataFrame, Series

from src.abis.load import WETH9_ADDRESS
from src.constants import COW_TOKEN_ADDRESS, COW_BONDING_POOL
from src.fetch.dune import DuneFetcher
from src.fetch.prices import eth_in_token, TokenId, token_in_eth
from src.fetch.prices import exchange_rate_atoms
from src.models.accounting_period import AccountingPeriod
from src.models.overdraft import Overdraft
from src.models.token import Token
Expand Down Expand Up @@ -261,7 +262,6 @@ class TokenConversion:
"""

eth_to_token: Callable
token_to_eth: Callable


def extend_payment_df(pdf: DataFrame, converter: TokenConversion) -> DataFrame:
Expand Down Expand Up @@ -457,9 +457,6 @@ def construct_payouts(
"""Workflow of solver reward payout logic post-CIP27"""
# pylint: disable-msg=too-many-locals

price_day = dune.period.end - timedelta(days=1)
reward_token = TokenId.COW

quote_rewards_df = orderbook.get_quote_rewards(dune.start_block, dune.end_block)
batch_rewards_df = orderbook.get_solver_rewards(dune.start_block, dune.end_block)
partner_fees_df = batch_rewards_df[["partner_list", "partner_fee_eth"]]
Expand Down Expand Up @@ -495,15 +492,22 @@ def construct_payouts(
# TODO - After CIP-20 phased in, adapt query to return `solver` like all the others
slippage_df = slippage_df.rename(columns={"solver_address": "solver"})

reward_token = COW_TOKEN_ADDRESS
native_token = Address(WETH9_ADDRESS)
price_day = dune.period.end - timedelta(days=1)
converter = TokenConversion(
eth_to_token=lambda t: exchange_rate_atoms(
native_token, reward_token, price_day
)
* t,
)

complete_payout_df = construct_payout_dataframe(
# Fetch and extend auction data from orderbook.
payment_df=extend_payment_df(
pdf=merged_df,
# provide token conversion functions (ETH <--> COW)
converter=TokenConversion(
eth_to_token=lambda t: eth_in_token(reward_token, t, price_day),
token_to_eth=lambda t: token_in_eth(reward_token, t, price_day),
),
converter=converter,
),
# Dune: Fetch Solver Slippage & Reward Targets
slippage_df=slippage_df,
Expand Down
41 changes: 18 additions & 23 deletions src/fetch/prices.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import logging.config
from datetime import datetime
from enum import Enum
from fractions import Fraction

from coinpaprika import client as cp
from dune_client.types import Address

from src.constants import LOG_CONFIG_FILE

Expand Down Expand Up @@ -41,32 +43,25 @@ def decimals(self) -> int:
return 18


def eth_in_token(quote_token: TokenId, amount: int, day: datetime) -> int:
"""
Compute how much of `token` is equivalent to `amount` ETH on `day`.
Use current price if day not specified.
"""
eth_amount_usd = token_in_usd(TokenId.ETH, amount, day)
quote_price_usd = token_in_usd(quote_token, 10 ** quote_token.decimals(), day)
return int(eth_amount_usd / quote_price_usd * 10 ** quote_token.decimals())


def token_in_eth(token: TokenId, amount: int, day: datetime) -> int:
"""
The inverse of eth_in_token;
how much ETH is equivalent to `amount` of `token` on `day`
"""
token_amount_usd = token_in_usd(token, amount, day)
eth_price_usd = token_in_usd(TokenId.ETH, 10 ** TokenId.ETH.decimals(), day)
TOKEN_ADDRESS_TO_ID = {
Address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"): TokenId.ETH,
Address("0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB"): TokenId.COW,
Address("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"): TokenId.USDC,
}

return int(token_amount_usd / eth_price_usd * 10 ** TokenId.ETH.decimals())


def token_in_usd(token: TokenId, amount_wei: int, day: datetime) -> float:
"""
Converts token amount [wei] to usd amount on given day.
def exchange_rate_atoms(
token_1_address: Address, token_2_address: Address, day: datetime
) -> Fraction:
"""Exchange rate for converting tokens on a given day.
The convention for the exchange rate r is as follows:
x atoms of token 1 have the same value as x * r atoms of token 2.
"""
return float(amount_wei) * usd_price(token, day) / 10.0 ** token.decimals()
token_1 = TOKEN_ADDRESS_TO_ID[token_1_address]
token_2 = TOKEN_ADDRESS_TO_ID[token_2_address]
price_1 = Fraction(usd_price(token_1, day)) / Fraction(10 ** token_1.decimals())
price_2 = Fraction(usd_price(token_2, day)) / Fraction(10 ** token_2.decimals())
return price_1 / price_2


@functools.cache
Expand Down
56 changes: 26 additions & 30 deletions tests/e2e/test_prices.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import unittest
from datetime import datetime, timedelta

from dune_client.types import Address

from src.abis.load import WETH9_ADDRESS
from src.constants import COW_TOKEN_ADDRESS
from src.fetch.prices import (
eth_in_token,
TOKEN_ADDRESS_TO_ID,
TokenId,
token_in_eth,
token_in_usd,
exchange_rate_atoms,
usd_price,
)

Expand All @@ -19,48 +22,41 @@ def setUp(self) -> None:
self.cow_price = usd_price(TokenId.COW, self.some_date)
self.eth_price = usd_price(TokenId.ETH, self.some_date)
self.usdc_price = usd_price(TokenId.USDC, self.some_date)
self.cow_address = COW_TOKEN_ADDRESS
self.weth_address = Address(WETH9_ADDRESS)
self.usdc_address = Address("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")

def test_usd_price(self):
self.assertEqual(self.usdc_price, 1.001622)
self.assertEqual(self.eth_price, 2481.89)
self.assertEqual(self.cow_price, 0.194899)

def test_token_in_usd(self):
def test_exchange_rate_atoms(self):
with self.assertRaises(AssertionError):
token_in_usd(TokenId.COW, ONE_ETH, datetime.today())
exchange_rate_atoms(self.cow_address, self.weth_address, datetime.today())

self.assertEqual(
token_in_usd(TokenId.ETH, ONE_ETH, self.some_date), self.eth_price
)
self.assertEqual(
token_in_usd(TokenId.COW, ONE_ETH, self.some_date), self.cow_price
exchange_rate_atoms(self.cow_address, self.cow_address, self.some_date), 1
)
self.assertEqual(
token_in_usd(TokenId.USDC, 10**6, self.some_date), self.usdc_price
exchange_rate_atoms(self.cow_address, self.weth_address, self.some_date),
1
/ exchange_rate_atoms(self.weth_address, self.cow_address, self.some_date),
)

def test_eth_in_token(self):
self.assertAlmostEqual(
eth_in_token(TokenId.COW, ONE_ETH, self.some_date) / 10**18,
self.eth_price / self.cow_price,
delta=DELTA,
)
self.assertAlmostEqual(
eth_in_token(TokenId.USDC, ONE_ETH, self.some_date) / 10**6,
self.eth_price / self.usdc_price,
delta=DELTA,
self.assertEqual(
float(
exchange_rate_atoms(self.cow_address, self.weth_address, self.some_date)
),
self.cow_price / self.eth_price,
)

def test_token_in_eth(self):
self.assertAlmostEqual(
token_in_eth(TokenId.COW, ONE_ETH, self.some_date),
10**18 * self.cow_price // self.eth_price,
delta=DELTA,
)
self.assertAlmostEqual(
token_in_eth(TokenId.USDC, 10**6, self.some_date),
10**18 * self.usdc_price // self.eth_price,
delta=DELTA,
self.assertEqual(
float(
exchange_rate_atoms(self.cow_address, self.usdc_address, self.some_date)
)
* 10**18,
self.cow_price / self.usdc_price * 10**6,
)

def test_price_cache(self):
Expand Down
4 changes: 1 addition & 3 deletions tests/unit/test_payouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ def setUp(self) -> None:
0.0,
]
# Mocking TokenConversion!
self.mock_converter = TokenConversion(
eth_to_token=lambda t: int(t * 1000), token_to_eth=lambda t: t // 1000
)
self.mock_converter = TokenConversion(eth_to_token=lambda t: int(t * 1000))

def test_extend_payment_df(self):
base_data_dict = {
Expand Down

0 comments on commit 59cb592

Please sign in to comment.