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

Compute partner fees #52

Merged
merged 28 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"0x875b6cb035bbd4ac6500fabc6d1e4ca5bdc58a3e2b424ccb5c24cdbebeb009a9"
)

NULL_ADDRESS = Web3.to_checksum_address("0x0000000000000000000000000000000000000000")

REQUEST_TIMEOUT = 5

# Time limit, currently set to 1 full day, after which Coingecko Token List is re-fetched (in seconds)
Expand Down
153 changes: 91 additions & 62 deletions src/fees/compute_fees.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
import math
import os
from typing import Any
import requests
import json
from dotenv import load_dotenv
from eth_typing import Address
from eth_typing import ChecksumAddress
from hexbytes import HexBytes
from web3 import Web3

from src.constants import (
REQUEST_TIMEOUT,
)
import requests
from src.constants import REQUEST_TIMEOUT, NULL_ADDRESS

# types for trades

Expand All @@ -21,17 +21,39 @@
class Trade:
"""Class for"""

order_uid: HexBytes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code now deviates sufficiently from the code in the circuit breaker to require actual tests.

sell_amount: int
buy_amount: int
sell_token: HexBytes
buy_token: HexBytes
limit_sell_amount: int
limit_buy_amount: int
kind: str
sell_token_clearing_price: int
buy_token_clearing_price: int
fee_policies: list["FeePolicy"]
def __init__(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a __post_init__ method could be used instead to set the remaining fields which require computation.

self,
order_uid: HexBytes,
sell_amount: int,
buy_amount: int,
sell_token: HexBytes,
buy_token: HexBytes,
limit_sell_amount: int,
limit_buy_amount: int,
kind: str,
sell_token_clearing_price: int,
buy_token_clearing_price: int,
fee_policies: list["FeePolicy"],
partner_fee_recipient: ChecksumAddress,
harisang marked this conversation as resolved.
Show resolved Hide resolved
):
self.order_uid = order_uid
self.sell_amount = sell_amount
self.buy_amount = buy_amount
self.sell_token = sell_token
self.buy_token = buy_token
self.limit_sell_amount = limit_sell_amount
self.limit_buy_amount = limit_buy_amount
self.kind = kind
self.sell_token_clearing_price = sell_token_clearing_price
self.buy_token_clearing_price = buy_token_clearing_price
self.fee_policies = fee_policies
self.partner_fee_recipient = partner_fee_recipient # if there is no partner, then its value is set to the null address

total_protocol_fee, partner_fee, network_fee = self.compute_all_fees()
self.total_protocol_fee = total_protocol_fee
self.partner_fee = partner_fee
self.network_fee = network_fee
return

def volume(self) -> int:
"""Compute volume of a trade in the surplus token"""
Expand Down Expand Up @@ -62,20 +84,31 @@ def surplus(self) -> int:
return current_limit_sell_amount - self.sell_amount
raise ValueError(f"Order kind {self.kind} is invalid.")

def raw_surplus(self) -> int:
"""Compute raw surplus of a trade in the surplus token
First, the application of protocol fees is reversed. Then, surplus of the resulting trade
is computed."""
def compute_all_fees(self) -> tuple[int, int, int]:
raw_trade = deepcopy(self)
for fee_policy in reversed(self.fee_policies):
raw_trade = fee_policy.reverse_protocol_fee(raw_trade)
return raw_trade.surplus()

def protocol_fee(self):
"""Compute protocol fees of a trade in the surplus token
Protocol fees are computed as the difference of raw surplus and surplus."""

return self.raw_surplus() - self.surplus()
total_protocol_fee = 0
partner_fee = 0
network_fee = 0
if self.fee_policies:
Copy link
Contributor

@fhenneke fhenneke Sep 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line required? If not, it should be removed. If yes, a small comment might help understand why it is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is line 98 that needs to execute after the for-loop finishes, but it only makes sense if there was at least one fee policy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, i see. But to me it is simpler to write

        partner_fee = 0
        for i, fee_policy in enumerate(reversed(self.fee_policies)):
            raw_trade = fee_policy.reverse_protocol_fee(raw_trade)
            ## we assume that partner fee is the last to be applied
            if i == 0 and self.partner_fee_recipient is not NULL_ADDRESS:
                partner_fee = raw_trade.surplus() - self.surplus()
        total_protocol_fee = raw_trade.surplus() - self.surplus()

The other two initializations are not required.

That part of the code might even benefit from a function

    def partner_fee(self):
        if not self.fee_policies or self.partner_fee_recipient is NULL_ADDRESS:
            return 0
        # we assume that partner fee is the last to be applied
        fee_policy = self.fee_policies[-1]
        raw_trade = deepcopy(self)
        raw_trade = fee_policy.reverse_protocol_fee(raw_trade)
        return raw_trade.surplus() - self.surplus()

and then just

        partner_fee = self.partner_fee()
        total_protocol_fee = self.total_protocol_fee()

The small loss in efficiency should not matter compared to the increase in readability. (Though you might disagree on the readability, then feel free to ignore).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually you are right and my concern was not really justified. I simplified the code and followed your first suggestion.

for i, fee_policy in enumerate(reversed(self.fee_policies)):
raw_trade = fee_policy.reverse_protocol_fee(raw_trade)
## we assume that partner fee is the last to be applied
if i == 0 and self.partner_fee_recipient is not NULL_ADDRESS:
partner_fee = raw_trade.surplus() - self.surplus()
total_protocol_fee = raw_trade.surplus() - self.surplus()

surplus_fee = self.compute_surplus_fee() # in the surplus token
network_fee_in_surplus_token = surplus_fee - self.total_protocol_fee
if self.kind == "sell":
network_fee = int(
network_fee_in_surplus_token
* Fraction(
self.buy_token_clearing_price, self.sell_token_clearing_price
)
)
else:
network_fee = network_fee_in_surplus_token
return total_protocol_fee, partner_fee, network_fee

def surplus_token(self) -> HexBytes:
"""Returns the surplus token"""
Expand Down Expand Up @@ -336,6 +369,14 @@ def get_all_data(self, tx_hash: HexBytes) -> SettlementData:
buy_token_clearing_price = clearing_prices[buy_token]
fee_policies = self.parse_fee_policies(trade_data["feePolicies"])

app_data = json.loads(order_data["fullAppData"])
if "partnerFee" in app_data["metadata"].keys():
partner_fee_recipient = Web3.to_checksum_address(
HexBytes(app_data["metadata"]["partnerFee"]["recipient"])
)
else:
partner_fee_recipient = NULL_ADDRESS

trade = Trade(
order_uid=uid,
sell_amount=executed_sell_amount,
Expand All @@ -348,6 +389,7 @@ def get_all_data(self, tx_hash: HexBytes) -> SettlementData:
sell_token_clearing_price=sell_token_clearing_price,
buy_token_clearing_price=buy_token_clearing_price,
fee_policies=fee_policies,
partner_fee_recipient=partner_fee_recipient,
)
trades.append(trade)

Expand Down Expand Up @@ -436,48 +478,35 @@ def parse_fee_policies(
return fee_policies


# computing fees
def compute_fee_imbalances(
settlement_data: SettlementData,
) -> tuple[dict[str, tuple[str, int]], dict[str, tuple[str, int]]]:
# function that computes all fees of all orders in a batch
# Note that currently it is NOT working for CoW AMMs as they are not indexed.
harisang marked this conversation as resolved.
Show resolved Hide resolved
def compute_all_fees_of_batch(
tx_hash: HexBytes,
) -> tuple[
dict[str, tuple[str, int]],
dict[str, tuple[str, int, str]],
dict[str, tuple[str, int]],
]:
orderbook_api = OrderbookFetcher()
settlement_data = orderbook_api.get_all_data(tx_hash)
Comment on lines +484 to +492
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combining the functions compute_fee_imbalances and batch_fee_imbalances entangles data fetching and fee computations. This makes it more difficult to to test the new convention for protocol fees.

protocol_fees: dict[str, tuple[str, int]] = {}
network_fees: dict[str, tuple[str, int]] = {}
partner_fees: dict[str, tuple[str, int, str]] = {}
for trade in settlement_data.trades:
# protocol fees
protocol_fee_amount = trade.protocol_fee()
protocol_fee_amount = trade.total_protocol_fee - trade.partner_fee
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this also breaks the convention (maybe we should!) with what we upload to Dune.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed but somehow it makes sense to have a table where when we look for a protocol fee, this is indeed the protocol fee and not something that potentially includes other fees there as well. So i would like to keep this here to be as explicit as possible in the db, I.e., have:

  • network fee in sell token
  • protocol fee in surplus token
  • partner fee, if any, in surplus token

And there are no subtractions or anything like that. Protocol fee goes to the DAO, partner fee goes to the partner, and network fee to solvers, and that's it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the convention on Dune is a bit misleading. Maybe we should switch it there as well. I am not familiar with the legal accounting requirements, though. Partner fees might be revenue with an associated cost.

Regarding partner fees going to partners: unfortunately that is not so easy as we take a cut. But generally I agree with this distinction. I expect the protocol fee mess to become even more ugly in the future, so having a reasonable intermediate representation sound most sustainable.

In terms of consistency with backend: the planned changes might include protocol fees per fee policy. So it is even one level more fine grained in case we go crazy with stacking protocol fees (which I expect to happen).

protocol_fee_token = trade.surplus_token()
protocol_fees[trade.order_uid.to_0x_hex()] = (
protocol_fee_token.to_0x_hex(),
protocol_fee_amount,
)
# network fees
surplus_fee = trade.compute_surplus_fee() # in the surplus token
network_fee = surplus_fee - protocol_fee_amount
if trade.kind == "sell":
network_fee_sell = int(
network_fee
* Fraction(
trade.buy_token_clearing_price, trade.sell_token_clearing_price
)
)
else:
network_fee_sell = network_fee

partner_fees[trade.order_uid.to_0x_hex()] = (
protocol_fee_token.to_0x_hex(),
trade.partner_fee,
trade.partner_fee_recipient,
)
network_fees[trade.order_uid.to_0x_hex()] = (
trade.sell_token.to_0x_hex(),
network_fee_sell,
trade.network_fee,
)

return protocol_fees, network_fees


# combined function


def batch_fee_imbalances(
tx_hash: HexBytes,
) -> tuple[dict[str, tuple[str, int]], dict[str, tuple[str, int]]]:
orderbook_api = OrderbookFetcher()
settlement_data = orderbook_api.get_all_data(tx_hash)
protocol_fees, network_fees = compute_fee_imbalances(settlement_data)
return protocol_fees, network_fees
return protocol_fees, partner_fees, network_fees
9 changes: 8 additions & 1 deletion src/helpers/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,29 @@ def write_fees(
token_address: str,
fee_amount: float,
fee_type: str,
recipient: str,
):
"""Function attempts to write price data to the table."""
tx_hash_bytes = bytes.fromhex(tx_hash[2:])
token_address_bytes = bytes.fromhex(token_address[2:])
order_uid_bytes = bytes.fromhex(order_uid[2:])

query = read_sql_file("src/sql/insert_fee.sql")
final_recipient = None
if recipient != "":
final_recipient = bytes.fromhex(recipient[2:])

self.execute_and_commit(
query,
{
"chain_name": self.chain_name,
"chain_name": chain_name,
harisang marked this conversation as resolved.
Show resolved Hide resolved
"auction_id": auction_id,
"block_number": block_number,
"tx_hash": tx_hash_bytes,
"order_uid": order_uid_bytes,
"token_address": token_address_bytes,
"fee_amount": fee_amount,
"fee_type": fee_type,
"recipient": final_recipient,
},
)
6 changes: 4 additions & 2 deletions src/test_single_hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from web3 import Web3
from src.imbalances_script import RawTokenImbalances
from src.price_providers.price_feed import PriceFeed
from src.fees.compute_fees import batch_fee_imbalances
from src.fees.compute_fees import compute_all_fees_of_batch
from src.transaction_processor import calculate_slippage
from src.helpers.config import get_web3_instance, logger
from contracts.erc20_abi import erc20_abi
Expand All @@ -26,7 +26,9 @@ def __init__(self):

def compute_data(self, tx_hash: str):
token_imbalances = self.imbalances.compute_imbalances(tx_hash)
protocol_fees, network_fees = batch_fee_imbalances(HexBytes(tx_hash))
protocol_fees, partner_fees, network_fees = compute_all_fees_of_batch(
HexBytes(tx_hash)
)
slippage = calculate_slippage(token_imbalances, protocol_fees, network_fees)
eth_slippage = self.calculate_slippage_in_eth(slippage, tx_hash)

Expand Down
55 changes: 45 additions & 10 deletions src/transaction_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from src.price_providers.price_feed import PriceFeed
from src.helpers.helper_functions import read_sql_file, set_params
from src.helpers.config import CHAIN_SLEEP_TIME, logger
from src.fees.compute_fees import batch_fee_imbalances
from src.fees.compute_fees import compute_all_fees_of_batch
import time


Expand Down Expand Up @@ -109,9 +109,11 @@ def process_single_transaction(

# Compute Fees
if self.process_fees:
protocol_fees, network_fees = self.process_fees_for_transaction(
tx_hash, auction_id, block_number
)
(
protocol_fees,
partner_fees,
network_fees,
) = self.process_fees_for_transaction(tx_hash)

# Compute Prices
if self.process_prices:
Expand All @@ -134,7 +136,12 @@ def process_single_transaction(

if self.process_fees:
self.handle_fees(
protocol_fees, network_fees, auction_id, block_number, tx_hash
protocol_fees,
partner_fees,
network_fees,
auction_id,
block_number,
tx_hash,
)

if self.process_prices and prices:
Expand Down Expand Up @@ -162,15 +169,22 @@ def process_token_imbalances(
return {}

def process_fees_for_transaction(
self, tx_hash: str, auction_id: int, block_number: int
) -> tuple[dict[str, tuple[str, int]], dict[str, tuple[str, int]]]:
self,
tx_hash: str,
) -> tuple[
dict[str, tuple[str, int]],
dict[str, tuple[str, int, str]],
dict[str, tuple[str, int]],
]:
"""Process and return protocol and network fees for a given transaction."""
try:
protocol_fees, network_fees = batch_fee_imbalances(HexBytes(tx_hash))
return protocol_fees, network_fees
protocol_fees, partner_fees, network_fees = compute_all_fees_of_batch(
HexBytes(tx_hash)
)
return protocol_fees, partner_fees, network_fees
except Exception as e:
logger.error(f"Failed to process fees for transaction {tx_hash}: {e}")
return {}, {}
return {}, {}, {}

def process_prices_for_tokens(
self,
Expand Down Expand Up @@ -224,6 +238,7 @@ def handle_imbalances(
def handle_fees(
self,
protocol_fees: dict[str, tuple[str, int]],
partner_fees: dict[str, tuple[str, int, str]],
network_fees: dict[str, tuple[str, int]],
auction_id: int,
block_number: int,
Expand All @@ -242,6 +257,25 @@ def handle_fees(
token_address=token_address,
fee_amount=float(fee_amount),
fee_type="protocol",
recipient="",
)

# Write partner fees
for order_uid, (
token_address,
fee_amount,
recipient,
) in partner_fees.items():
self.db.write_fees(
chain_name=self.chain_name,
auction_id=auction_id,
block_number=block_number,
tx_hash=tx_hash,
order_uid=order_uid,
token_address=token_address,
fee_amount=float(fee_amount),
fee_type="partner",
recipient=recipient,
)

# Write network fees
Expand All @@ -255,6 +289,7 @@ def handle_fees(
token_address=token_address,
fee_amount=float(fee_amount),
fee_type="network",
recipient="",
)
except Exception as err:
logger.error(
Expand Down
Loading