From a4e9be6198a6f0567e0838eab3f4773512f5bbb4 Mon Sep 17 00:00:00 2001 From: Felix Henneke Date: Fri, 8 Nov 2024 01:45:53 +0100 Subject: [PATCH 1/2] Revert change about filtering dataframes (#417) This PR reverts a change introduces in #407. The PR introduced a somewhat unrelated change to filter duplicate columns from data frames before merging. In local testing it seems that the data frames do not actually contain such columns. Thus, the somewhat difficult to read code fragment does not have any effect. If the code is indeed required, I would suggest restructuring it for clarity. If it is important that this column only appears in one data frame, one could add a check to `validate_df_columns`. Co-authored-by: Haris Angelidakis <64154020+harisang@users.noreply.github.com> --- src/fetch/payouts.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/fetch/payouts.py b/src/fetch/payouts.py index 6586f267..6e3b48cd 100644 --- a/src/fetch/payouts.py +++ b/src/fetch/payouts.py @@ -399,19 +399,10 @@ def construct_payout_dataframe( normalize_address_field(service_fee_df, join_column) # 3. Merge the three dataframes (joining on solver) - - reward_target_reduced_df_columns = [ - x for x in list(reward_target_df.columns) if x != "solver_name" - ] - reward_target_reduced_df = reward_target_df[reward_target_reduced_df_columns] - service_fee_reduced_df_columns = [ - x for x in list(service_fee_df.columns) if x != "solver_name" - ] - service_fee_reduced_df = service_fee_df[service_fee_reduced_df_columns] merged_df = ( payment_df.merge(slippage_df, on=join_column, how="left") - .merge(reward_target_reduced_df, on=join_column, how="left") - .merge(service_fee_reduced_df, on=join_column, how="left") + .merge(reward_target_df, on=join_column, how="left") + .merge(service_fee_df, on=join_column, how="left") ) # 4. Add slippage from fees to slippage From 59cb5929f44102daec1ecc8a9cf0a458a97c1d5a Mon Sep 17 00:00:00 2001 From: Felix Henneke Date: Sun, 10 Nov 2024 16:48:55 +0100 Subject: [PATCH 2/2] Restructure price fetching (#421) 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 <64154020+harisang@users.noreply.github.com> --- src/fetch/payouts.py | 22 +++++++++------ src/fetch/prices.py | 41 ++++++++++++---------------- tests/e2e/test_prices.py | 56 ++++++++++++++++++-------------------- tests/unit/test_payouts.py | 4 +-- 4 files changed, 58 insertions(+), 65 deletions(-) diff --git a/src/fetch/payouts.py b/src/fetch/payouts.py index 6e3b48cd..644fd3b1 100644 --- a/src/fetch/payouts.py +++ b/src/fetch/payouts.py @@ -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 @@ -261,7 +262,6 @@ class TokenConversion: """ eth_to_token: Callable - token_to_eth: Callable def extend_payment_df(pdf: DataFrame, converter: TokenConversion) -> DataFrame: @@ -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"]] @@ -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, diff --git a/src/fetch/prices.py b/src/fetch/prices.py index 97442d87..e910124c 100644 --- a/src/fetch/prices.py +++ b/src/fetch/prices.py @@ -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 @@ -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 diff --git a/tests/e2e/test_prices.py b/tests/e2e/test_prices.py index b3bc8b9b..13cf7cc5 100644 --- a/tests/e2e/test_prices.py +++ b/tests/e2e/test_prices.py @@ -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, ) @@ -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): diff --git a/tests/unit/test_payouts.py b/tests/unit/test_payouts.py index 1d4a07cc..5bfad642 100644 --- a/tests/unit/test_payouts.py +++ b/tests/unit/test_payouts.py @@ -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 = {