Skip to content

Commit

Permalink
Merge branch 'main' into no_service_fee_on_penalty
Browse files Browse the repository at this point in the history
  • Loading branch information
harisang authored Nov 11, 2024
2 parents 9f98c5b + 3645d92 commit 37f4054
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 130 deletions.
118 changes: 74 additions & 44 deletions src/fetch/payouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
from fractions import Fraction
from typing import Callable

import numpy as np
import pandas
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 @@ -44,8 +46,13 @@
}
REWARD_TARGET_COLUMNS = {"solver", "reward_target", "pool_address"}
SERVICE_FEE_COLUMNS = {"solver", "service_fee"}
ADDITIONAL_PAYMENT_COLUMNS = {"buffer_accounting_target", "reward_token_address"}

COMPLETE_COLUMNS = PAYMENT_COLUMNS.union(SLIPPAGE_COLUMNS).union(REWARD_TARGET_COLUMNS)
COMPLETE_COLUMNS = (
PAYMENT_COLUMNS.union(SLIPPAGE_COLUMNS)
.union(REWARD_TARGET_COLUMNS)
.union(ADDITIONAL_PAYMENT_COLUMNS)
)
NUMERICAL_COLUMNS = [
"primary_reward_eth",
"primary_reward_cow",
Expand All @@ -72,26 +79,28 @@ def __init__( # pylint: disable=too-many-arguments
self,
solver: Address,
solver_name: str,
reward_target: Address,
bonding_pool: Address,
reward_target: Address, # recipient address of rewards
buffer_accounting_target: Address, # recipient address of net buffer changes
primary_reward_eth: int,
slippage_eth: int,
primary_reward_cow: int,
quote_reward_cow: int,
service_fee: bool,
service_fee: Fraction,
reward_token_address: Address,
):

assert quote_reward_cow >= 0, "invalid quote_reward_cow"

self.solver = solver
self.solver_name = solver_name
self.reward_target = reward_target
self.bonding_pool = bonding_pool
self.buffer_accounting_target = buffer_accounting_target
self.slippage_eth = slippage_eth
self.primary_reward_eth = primary_reward_eth
self.primary_reward_cow = primary_reward_cow
self.quote_reward_cow = quote_reward_cow
self.service_fee = service_fee
self.reward_token_address = reward_token_address

@classmethod
def from_series(cls, frame: Series) -> RewardAndPenaltyDatum:
Expand All @@ -103,21 +112,28 @@ def from_series(cls, frame: Series) -> RewardAndPenaltyDatum:
)
solver = frame["solver"]
reward_target = frame["reward_target"]
bonding_pool = frame["pool_address"]
if reward_target is None:
logging.warning(f"solver {solver} without reward_target. Using solver")
logging.warning(f"Solver {solver} without reward_target. Using solver")
reward_target = solver

buffer_accounting_target = frame["buffer_accounting_target"]
if buffer_accounting_target is None:
logging.warning(
f"Solver {solver} without buffer_accounting_target. Using solver"
)
buffer_accounting_target = solver

return cls(
solver=Address(solver),
solver_name=frame["solver_name"],
reward_target=Address(reward_target),
bonding_pool=Address(bonding_pool),
buffer_accounting_target=Address(buffer_accounting_target),
slippage_eth=slippage,
primary_reward_eth=int(frame["primary_reward_eth"]),
primary_reward_cow=int(frame["primary_reward_cow"]),
quote_reward_cow=int(frame["quote_reward_cow"]),
service_fee=bool(frame["service_fee"]),
service_fee=Fraction(frame["service_fee"]),
reward_token_address=Address(frame["reward_token_address"]),
)

def total_outgoing_eth(self) -> int:
Expand All @@ -143,7 +159,15 @@ def total_eth_reward(self) -> int:
def reward_scaling(self) -> Fraction:
"""Scaling factor for service fee
The reward is multiplied by this factor"""
return 1 - SERVICE_FEE_FACTOR * self.service_fee
return 1 - self.service_fee

def total_service_fee(self) -> Fraction:
"""Total service fee charged from rewards"""
return (
SERVICE_FEE_FACTOR
* self.service_fee
* (self.primary_reward_cow + self.quote_reward_cow)
)

def total_service_fee(self) -> Fraction:
"""Scaling factor for service fee
Expand All @@ -170,7 +194,7 @@ def as_payouts(self) -> list[Transfer]:
if quote_reward_cow > 0:
result.append(
Transfer(
token=Token(COW_TOKEN_ADDRESS),
token=Token(self.reward_token_address),
recipient=self.reward_target,
amount_wei=quote_reward_cow,
)
Expand Down Expand Up @@ -203,11 +227,7 @@ def as_payouts(self) -> list[Transfer]:
result.append(
Transfer(
token=None,
recipient=(
self.reward_target
if self.bonding_pool == COW_BONDING_POOL
else self.solver
),
recipient=(self.buffer_accounting_target),
amount_wei=reimbursement_eth + total_eth_reward,
)
)
Expand All @@ -226,7 +246,7 @@ def as_payouts(self) -> list[Transfer]:
try:
result.append(
Transfer(
token=Token(COW_TOKEN_ADDRESS),
token=Token(self.reward_token_address),
recipient=self.reward_target,
amount_wei=reimbursement_cow + total_cow_reward,
)
Expand All @@ -243,11 +263,7 @@ def as_payouts(self) -> list[Transfer]:
result.append(
Transfer(
token=None,
recipient=(
self.reward_target
if self.bonding_pool == COW_BONDING_POOL
else self.solver
),
recipient=(self.buffer_accounting_target),
amount_wei=reimbursement_eth,
)
)
Expand All @@ -258,7 +274,7 @@ def as_payouts(self) -> list[Transfer]:
try:
result.append(
Transfer(
token=Token(COW_TOKEN_ADDRESS),
token=Token(self.reward_token_address),
recipient=self.reward_target,
amount_wei=total_cow_reward,
)
Expand All @@ -278,7 +294,6 @@ class TokenConversion:
"""

eth_to_token: Callable
token_to_eth: Callable


def extend_payment_df(pdf: DataFrame, converter: TokenConversion) -> DataFrame:
Expand Down Expand Up @@ -416,26 +431,36 @@ 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
merged_df["eth_slippage_wei"] = (
merged_df["eth_slippage_wei"].fillna(0) + merged_df["network_fee_eth"]
)

# 5. Compute buffer accounting target
merged_df["buffer_accounting_target"] = np.where(
merged_df["pool_address"] != COW_BONDING_POOL.address,
merged_df["solver"],
merged_df["reward_target"],
)

# 6. Add reward token address
merged_df["reward_token_address"] = COW_TOKEN_ADDRESS.address

# 7. Missing service fee is treated as new solver
if any(merged_df["service_fee"].isna()):
missing_solvers = merged_df["solver"].loc[merged_df["service_fee"].isna()]
logging.warning(
f"Solvers {missing_solvers} without service fee info. Using 0%. "
f"Check service fee query."
)
merged_df["service_fee"] = merged_df["service_fee"].fillna(Fraction(0, 1)) # type: ignore

return merged_df


Expand Down Expand Up @@ -483,9 +508,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 All @@ -501,7 +523,8 @@ def construct_payouts(

service_fee_df = pandas.DataFrame(dune.get_service_fee_status())
service_fee_df["service_fee"] = [
datetime.strptime(time_string, "%Y-%m-%d %H:%M:%S.%f %Z") <= dune.period.start
(datetime.strptime(time_string, "%Y-%m-%d %H:%M:%S.%f %Z") <= dune.period.start)
* SERVICE_FEE_FACTOR
for time_string in service_fee_df["expires"]
]
reward_target_df = pandas.DataFrame(dune.get_vouches())
Expand All @@ -521,15 +544,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
Loading

0 comments on commit 37f4054

Please sign in to comment.