Skip to content

Commit

Permalink
Add protocol fee (#333)
Browse files Browse the repository at this point in the history
This PR adds changes to solver rewards due to the introduction of
protocol fees.

The implementation in this PR is intended to make solvers oblivious to
protocol fees. This means that the quality of a solution as defined in
CIP-20 in our accounting is needs to be equal to what the solver
computed without knowledge of protocol fees.

CIP-20 specifies that `quality = surplus + fee` in ETH. With protocol
fees, the surplus is reduced by some amount, the protocol fee. This
means that the quality is now something like `quality = surplus +
protocol_fee + fee` in ETH. Here, `surplus` refers to the surplus users
receive, `protocol_fee` is the fee the protocol charges, and `fee` is a
fee set by solvers or the protocol for covering network costs. All token
amounts are converted to ETH using native prices of the auction.

One problem is, that fees set by solvers for covering network costs are
not directly observable for the autopilot. For those orders, the
autopilot only computes a total fee in the sell token by comparing what
a user did send to the protocol to what they would have sent to receive
the same amount of buy tokens if they had traded at uniform clearing
prices. Let us call this term `total_fee` and it is in the sell token.
Let us further use the term `total_surplus` for the surplus a solver
perceives who is oblivious to protocol fees. The inconsistency corrected
in this PR comes from the fact that the solver perceives a quality of
`total_surplus * surplus_native_price + fee * sell_native_price`. This
is different from `surplus * surplus_native_price + total_fee *
sell_native_price`. Instead, the reward script needs to compute `surplus
* surplus_native_price + protocol_fee * protocol_fee_native_price + fee
* sell_native_price`. The fee for network costs needs to be
reconstructed from observations. The driver currently computes the total
fee in the sell token as `total_fee = fee + protocol_fee *
protocol_fee_clearing_price / sell_clearing_price`. Thus the network fee
is `total_fee - protocol_fee * protocol_fee_clearing_price /
sell_clearing_price`. Since the ratio of clearing prices is equal to the
ratio of traded amount _if there were no fee at all,_
`protocol_fee_clearing_price / sell_clearing_price = (amount_sent -
total_fee) / amount_received`, the correction to the total fee to
compute the actual fee for network costs becomes `network_fee_correction
= (amount_sent - total_fee) / amount_received * protocol_fee`.

The implementation in this PR depends on the current implementation of
protocol fees in the driver. With drivers ran by solvers, this is not
possible anymore. By then we should be ready to rank by `surplus +
protocol_fee`. This value is easy to compute for the driver and no
correction of network fees is required anymore.
  • Loading branch information
fhenneke authored Feb 5, 2024
1 parent 09804a7 commit 2bda2b1
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 17 deletions.
152 changes: 141 additions & 11 deletions queries/orderbook/batch_rewards.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,130 @@ WITH observed_settlements AS (SELECT
WHERE block_deadline >= {{start_block}}
AND block_deadline <= {{end_block}}
GROUP BY ss.auction_id),
-- protocol fees:
order_surplus AS (
SELECT
ss.winner as solver,
at.auction_id,
s.tx_hash,
t.order_uid,
o.sell_token,
o.buy_token,
t.sell_amount, -- the total amount the user sends
t.buy_amount, -- the total amount the user receives
oe.surplus_fee as observed_fee, -- the total discrepancy between what the user sends and what they would have send if they traded at clearing price
o.kind,
CASE
WHEN o.kind = 'sell'
THEN t.buy_amount - t.sell_amount * o.buy_amount / (o.sell_amount + o.fee_amount)
WHEN o.kind = 'buy'
THEN t.buy_amount * (o.sell_amount + o.fee_amount) / o.buy_amount - t.sell_amount
END AS surplus,
CASE
WHEN o.kind = 'sell'
THEN o.buy_token
WHEN o.kind = 'buy'
THEN o.sell_token
END AS surplus_token
FROM settlements s -- links block_number and log_index to tx_from and tx_nonce
JOIN auction_transaction at -- links auction_id to tx_from and tx_nonce
ON s.tx_from = at.tx_from AND s.tx_nonce = at.tx_nonce
JOIN settlement_scores ss -- contains block_deadline
ON at.auction_id = ss.auction_id
JOIN trades t -- contains traded amounts
ON s.block_number = t.block_number -- log_index cannot be checked, does not work correctly with multiple auctions on the same block
JOIN orders o -- contains tokens and limit amounts
ON t.order_uid = o.uid
JOIN order_execution oe -- contains surplus fee
ON t.order_uid = oe.order_uid AND at.auction_id = oe.auction_id
WHERE ss.block_deadline >= {{start_block}}
AND ss.block_deadline <= {{end_block}}
)
,order_protocol_fee AS (
SELECT
os.auction_id,
os.solver,
os.tx_hash,
os.sell_amount,
os.buy_amount,
os.sell_token,
os.observed_fee,
os.surplus,
os.surplus_token,
CASE
WHEN fp.kind = 'surplus'
THEN
CASE
WHEN os.kind = 'sell'
THEN
-- We assume that the case surplus_factor != 1 always. In
-- that case reconstructing the protocol fee would be
-- impossible anyways. This query will return a division by
-- zero error in that case.
LEAST(
fp.max_volume_factor * os.sell_amount * os.buy_amount / (os.sell_amount - os.observed_fee), -- at most charge a fraction of volume
fp.surplus_factor / (1 - fp.surplus_factor) * surplus -- charge a fraction of surplus
)
WHEN os.kind = 'buy'
THEN
LEAST(
fp.max_volume_factor / (1 + fp.max_volume_factor) * os.sell_amount, -- at most charge a fraction of volume
fp.surplus_factor / (1 - fp.surplus_factor) * surplus -- charge a fraction of surplus
)
END
WHEN fp.kind = 'volume'
THEN fp.volume_factor / (1 + fp.volume_factor) * os.sell_amount
END AS protocol_fee,
CASE
WHEN fp.kind = 'surplus'
THEN os.surplus_token
WHEN fp.kind = 'volume'
THEN os.sell_token
END AS protocol_fee_token
FROM order_surplus os
JOIN fee_policies fp -- contains protocol fee policy
ON os.auction_id = fp.auction_id AND os.order_uid = fp.order_uid
)
,order_protocol_fee_prices AS (
SELECT
opf.solver,
opf.tx_hash,
opf.surplus,
opf.protocol_fee,
CASE
WHEN opf.sell_token != opf.protocol_fee_token
THEN (opf.sell_amount - opf.observed_fee) / opf.buy_amount * opf.protocol_fee
ELSE opf.protocol_fee
END AS network_fee_correction,
opf.sell_token as network_fee_token,
ap_surplus.price / pow(10, 18) as surplus_token_price,
ap_protocol.price / pow(10, 18) as protocol_fee_token_price,
ap_sell.price / pow(10, 18) as network_fee_token_price
FROM order_protocol_fee opf
JOIN auction_prices ap_sell -- contains price: sell token
ON opf.auction_id = ap_sell.auction_id AND opf.sell_token = ap_sell.token
JOIN auction_prices ap_surplus -- contains price: surplus token
ON opf.auction_id = ap_surplus.auction_id AND opf.surplus_token = ap_surplus.token
JOIN auction_prices ap_protocol -- contains price: protocol fee token
ON opf.auction_id = ap_protocol.auction_id AND opf.protocol_fee_token = ap_protocol.token
),
batch_protocol_fees AS (
SELECT
solver,
tx_hash,
-- sum(surplus * surplus_token_price) as surplus,
sum(protocol_fee * protocol_fee_token_price) as protocol_fee,
sum(network_fee_correction * network_fee_token_price) as network_fee_correction
FROM order_protocol_fee_prices
group by solver, tx_hash
),
reward_data AS (SELECT
-- observations
tx_hash,
os.tx_hash,
ss.auction_id,
-- TODO - Assuming that `solver == winner` when both not null
-- We will need to monitor that `solver == winner`!
coalesce(solver, winner) as solver,
coalesce(os.solver, winner) as solver,
block_number as settlement_block,
block_deadline,
case
Expand All @@ -50,26 +167,32 @@ WITH observed_settlements AS (SELECT
winning_score,
reference_score,
-- auction_participation
participating_solvers
participating_solvers,
-- protocol_fees
coalesce(cast(protocol_fee as numeric(78, 0)), 0) as protocol_fee,
coalesce(cast(network_fee_correction as numeric(78, 0)), 0) as network_fee_correction
FROM settlement_scores ss
-- If there are reported scores,
-- there will always be a record of auction participants
JOIN auction_participation ap
ON ss.auction_id = ap.auction_id
-- outer joins made in order to capture non-existent settlements.
LEFT OUTER JOIN observed_settlements os
ON os.auction_id = ss.auction_id),
ON os.auction_id = ss.auction_id
LEFT OUTER JOIN batch_protocol_fees bpf
ON bpf.tx_hash = os.tx_hash),
reward_per_auction as (SELECT tx_hash,
auction_id,
settlement_block,
block_deadline,
solver,
execution_cost,
surplus,
fee,
surplus + fee - reference_score as uncapped_reward_eth,
-- Uncapped Reward = CLAMP_[-E, E + exec_cost](uncapped_reward_eth)
LEAST(GREATEST(-{{EPSILON}}, surplus + fee - reference_score),
protocol_fee,
fee - network_fee_correction as network_fee,
surplus + protocol_fee + fee - network_fee_correction - reference_score as uncapped_reward_eth,
-- capped Reward = CLAMP_[-E, E + exec_cost](uncapped_reward_eth)
LEAST(GREATEST(-{{EPSILON}}, surplus + coalesce(protocol_fee - network_fee_correction, 0) + fee - reference_score),
{{EPSILON}} + execution_cost) as capped_payment,
winning_score,
reference_score,
Expand All @@ -85,14 +208,21 @@ WITH observed_settlements AS (SELECT
SUM(execution_cost) as exececution_cost_wei
FROM reward_per_auction rpt
GROUP BY solver),
protocol_fees as (SELECT solver,
SUM(protocol_fee) as protocol_fee_wei
FROM reward_per_auction rpt
GROUP BY solver),
aggregate_results as (SELECT concat('0x', encode(pc.solver, 'hex')) as solver,
coalesce(payment_wei, 0) as payment_eth,
coalesce(exececution_cost_wei, 0) as execution_cost_eth,
num_participating_batches
num_participating_batches,
coalesce(protocol_fee_wei, 0) as protocol_fee_eth
FROM participation_counts pc
LEFT OUTER JOIN primary_rewards pr
ON pr.solver = pc.solver)

ON pr.solver = pc.solver
LEFT OUTER JOIN protocol_fees pf
ON pf.solver = pc.solver)
--
select *
from aggregate_results
order by solver;
16 changes: 15 additions & 1 deletion src/fetch/payouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
PERIOD_BUDGET_COW = 306646 * 10**18
QUOTE_REWARD = 9 * 10**18

PROTOCOL_FEE_SAFE = Address("0xB64963f95215FDe6510657e719bd832BB8bb941B")

PAYMENT_COLUMNS = {
"solver",
"payment_eth",
Expand All @@ -33,6 +35,7 @@
"reward_cow",
"secondary_reward_cow",
"quote_reward_cow",
"protocol_fee_eth",
}
SLIPPAGE_COLUMNS = {
"solver",
Expand All @@ -50,6 +53,7 @@
"secondary_reward_cow",
"secondary_reward_eth",
"quote_reward_cow",
"protocol_fee_eth",
]


Expand Down Expand Up @@ -308,6 +312,14 @@ def prepare_transfers(payout_df: DataFrame, period: AccountingPeriod) -> PeriodP
overdrafts.append(overdraft)
transfers += payout_datum.as_payouts()

transfers.append(
Transfer(
token=None,
recipient=PROTOCOL_FEE_SAFE,
amount_wei=int(payout_df.protocol_fee_eth.sum()),
)
)

return PeriodPayouts(overdrafts, transfers)


Expand Down Expand Up @@ -396,10 +408,12 @@ def construct_payouts(
performance_reward = complete_payout_df["reward_cow"].sum()
participation_reward = complete_payout_df["secondary_reward_cow"].sum()
quote_reward = complete_payout_df["quote_reward_cow"].sum()
protocol_fee = complete_payout_df["protocol_fee_eth"].sum()
dune.log_saver.print(
f"Performance Reward: {performance_reward / 10 ** 18:.4f}\n"
f"Participation Reward: {participation_reward / 10 ** 18:.4f}\n"
f"Quote Reward: {quote_reward / 10 ** 18:.4f}\n",
f"Quote Reward: {quote_reward / 10 ** 18:.4f}\n"
f"Protocol Fees: {protocol_fee / 10 ** 18:.4f}\n",
category=Category.TOTALS,
)
payouts = prepare_transfers(complete_payout_df, dune.period)
Expand Down
Loading

0 comments on commit 2bda2b1

Please sign in to comment.