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 10 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
114 changes: 75 additions & 39 deletions src/fees/compute_fees.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import os
from typing import Any
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,
)
from src.constants import REQUEST_TIMEOUT, NULL_ADDRESS
import requests
import json
harisang marked this conversation as resolved.
Show resolved Hide resolved

# 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
self.network_fee: int = -1
self.protocol_fee: int = -1
self.partner_fee: int = -1

self.compute_all_fees()
harisang marked this conversation as resolved.
Show resolved Hide resolved
return

def volume(self) -> int:
"""Compute volume of a trade in the surplus token"""
Expand Down Expand Up @@ -62,20 +84,38 @@ 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) -> None:
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()
i = 0
self.protocol_fee = 0
self.partner_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 fee_policy in 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:
self.partner_fee = raw_trade.surplus() - self.surplus()
i = i + 1
harisang marked this conversation as resolved.
Show resolved Hide resolved
self.protocol_fee = raw_trade.surplus() - self.surplus() - self.partner_fee
harisang marked this conversation as resolved.
Show resolved Hide resolved

surplus_fee = self.compute_surplus_fee() # in the surplus token
network_fee_temp = surplus_fee - (self.protocol_fee + self.partner_fee)
harisang marked this conversation as resolved.
Show resolved Hide resolved
if self.kind == "sell":
self.network_fee = int(
network_fee_temp
* Fraction(
self.buy_token_clearing_price, self.sell_token_clearing_price
)
)
else:
self.network_fee = network_fee_temp
return

def protocol_fee(self):
def total_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()
return self.protocol_fee + self.partner_fee

def surplus_token(self) -> HexBytes:
"""Returns the surplus token"""
Expand Down Expand Up @@ -336,6 +376,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 +396,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 @@ -444,28 +493,15 @@ def compute_fee_imbalances(
network_fees: dict[str, tuple[str, int]] = {}
for trade in settlement_data.trades:
# protocol fees
protocol_fee_amount = trade.protocol_fee()
protocol_fee_amount = trade.protocol_fee
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

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
Expand Down
Loading