Skip to content

Commit

Permalink
Allow whitelisting addresses for protocol fees discounts (#2573)
Browse files Browse the repository at this point in the history
# Description
Allow configuring contracts which are exempt from protocol fees.
Our CoW AMM experiment is placing a lot of limit orders, some of which
are being classified as out of market orders (mainly because they are
partially fillable and usually a bit larger than what is currently
fillable at the specified price).
It therefore pays significant protocol fees, impacting it's own PnL

# Changes
- Configure the driver to accept whitelisted address for protocol fee
discount
- Skip protocol fee for whitelisted addresses

## How to test
1. e2e tests
2. unit tests

Fixes #2461
  • Loading branch information
m-lord-renkse authored Apr 5, 2024
1 parent 230f39d commit 6c17db6
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 54 deletions.
11 changes: 11 additions & 0 deletions crates/autopilot/src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ pub struct Arguments {
#[clap(long, env, default_value = "0.01")]
pub fee_policy_max_partner_fee: FeeFactor,

/// List of addresses which are exempt from the protocol
/// fees
#[clap(long, env, use_value_delimiter = true)]
pub protocol_fee_exempt_addresses: Vec<H160>,

/// Arguments for uploading information to S3.
#[clap(flatten)]
pub s3: infra::persistence::cli::S3,
Expand Down Expand Up @@ -262,6 +267,7 @@ impl std::fmt::Display for Arguments {
auction_update_interval,
max_settlement_transaction_wait,
s3,
protocol_fee_exempt_addresses,
} = self;

write!(f, "{}", shared)?;
Expand Down Expand Up @@ -314,6 +320,11 @@ impl std::fmt::Display for Arguments {
display_option(f, "shadow", shadow)?;
writeln!(f, "solve_deadline: {:?}", solve_deadline)?;
writeln!(f, "fee_policies: {:?}", fee_policies)?;
writeln!(
f,
"protocol_fee_exempt_addresses: {:?}",
protocol_fee_exempt_addresses
)?;
writeln!(
f,
"fee_policy_max_partner_fee: {:?}",
Expand Down
71 changes: 42 additions & 29 deletions crates/autopilot/src/domain/fee/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ mod policy;

use {
crate::{
arguments,
arguments::{self},
boundary::{self},
domain::{self},
},
app_data::Validator,
derive_more::Into,
itertools::Itertools,
primitive_types::U256,
primitive_types::{H160, U256},
prometheus::core::Number,
std::str::FromStr,
std::{collections::HashSet, str::FromStr},
};

enum OrderClass {
Expand Down Expand Up @@ -51,33 +51,39 @@ impl From<arguments::FeePolicy> for ProtocolFee {
}
}

pub type ProtocolFeeExemptAddresses = HashSet<H160>;

pub struct ProtocolFees {
fee_policies: Vec<ProtocolFee>,
max_partner_fee: FeeFactor,
/// List of addresses which are exempt from the protocol
/// fees
protocol_fee_exempt_addresses: ProtocolFeeExemptAddresses,
}

impl ProtocolFees {
pub fn new(
fee_policies: &[arguments::FeePolicy],
fee_policy_max_partner_fee: FeeFactor,
protocol_fee_exempt_addresses: &[H160],
) -> Self {
Self {
fee_policies: fee_policies
.iter()
.cloned()
.map(ProtocolFee::from)
.collect(),
protocol_fee_exempt_addresses: protocol_fee_exempt_addresses
.iter()
.cloned()
.collect::<HashSet<_>>(),
max_partner_fee: fee_policy_max_partner_fee,
}
}

/// Converts an order from the boundary layer to the domain layer, applying
/// protocol fees if necessary.
pub fn apply(
protocol_fees: &ProtocolFees,
order: boundary::Order,
quote: &domain::Quote,
) -> domain::Order {
pub fn apply(&self, order: boundary::Order, quote: &domain::Quote) -> domain::Order {
// If the partner fee is specified, it overwrites the current volume fee policy
if let Some(validated_app_data) = order
.metadata
Expand All @@ -92,7 +98,7 @@ impl ProtocolFees {
let fee_policy = vec![Policy::Volume {
factor: FeeFactor::try_from_capped(
partner_fee.bps.into_f64() / 10_000.0,
protocol_fees.max_partner_fee.into(),
self.max_partner_fee.into(),
)
.unwrap(),
}];
Expand All @@ -110,26 +116,33 @@ impl ProtocolFees {
buy: quote.buy_amount,
fee: quote.fee,
};
let protocol_fees = protocol_fees
.fee_policies
.iter()
// TODO: support multiple fee policies
.find_map(|fee_policy| {
let outside_market_price = boundary::is_order_outside_market_price(&order_, &quote_, order.data.kind);
match (outside_market_price, &fee_policy.order_class) {
(_, OrderClass::Any) => Some(&fee_policy.policy),
(true, OrderClass::Limit) => Some(&fee_policy.policy),
(false, OrderClass::Market) => Some(&fee_policy.policy),
_ => None,
}
})
.and_then(|policy| match policy {
policy::Policy::Surplus(variant) => variant.apply(&order),
policy::Policy::PriceImprovement(variant) => variant.apply(&order, quote),
policy::Policy::Volume(variant) => variant.apply(&order),
})
.into_iter()
.collect_vec();
let protocol_fees = if self
.protocol_fee_exempt_addresses
.contains(&order.metadata.owner)
{
vec![]
} else {
self
.fee_policies
.iter()
// TODO: support multiple fee policies
.find_map(|fee_policy| {
let outside_market_price = boundary::is_order_outside_market_price(&order_, &quote_, order.data.kind);
match (outside_market_price, &fee_policy.order_class) {
(_, OrderClass::Any) => Some(&fee_policy.policy),
(true, OrderClass::Limit) => Some(&fee_policy.policy),
(false, OrderClass::Market) => Some(&fee_policy.policy),
_ => None,
}
})
.and_then(|policy| match policy {
policy::Policy::Surplus(variant) => variant.apply(&order),
policy::Policy::PriceImprovement(variant) => variant.apply(&order, quote),
policy::Policy::Volume(variant) => variant.apply(&order),
})
.into_iter()
.collect_vec()
};
boundary::order::to_domain(order, protocol_fees)
}
}
Expand Down
6 changes: 5 additions & 1 deletion crates/autopilot/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,11 @@ pub async fn run(args: Arguments) {
args.limit_order_price_factor
.try_into()
.expect("limit order price factor can't be converted to BigDecimal"),
domain::ProtocolFees::new(&args.fee_policies, args.fee_policy_max_partner_fee),
domain::ProtocolFees::new(
&args.fee_policies,
args.fee_policy_max_partner_fee,
args.protocol_fee_exempt_addresses.as_slice(),
),
);

let liveness = Arc::new(Liveness::new(args.max_auction_age));
Expand Down
4 changes: 2 additions & 2 deletions crates/autopilot/src/solvable_orders.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use {
crate::{
domain,
domain::{self},
infra::{self, banned},
},
anyhow::Result,
Expand Down Expand Up @@ -242,7 +242,7 @@ impl SolvableOrdersCache {
.into_iter()
.filter_map(|order| {
if let Some(quote) = db_solvable_orders.quotes.get(&order.metadata.uid.into()) {
Some(domain::ProtocolFees::apply(&self.protocol_fees, order, quote))
Some(self.protocol_fees.apply(order, quote))
} else {
tracing::warn!(order_uid = %order.metadata.uid, "order is skipped, quote is missing");
None
Expand Down
81 changes: 59 additions & 22 deletions crates/e2e/tests/e2e/protocol_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,17 @@ async fn combined_protocol_fees(web3: Web3) {
let mut onchain = OnchainComponents::deploy(web3.clone()).await;

let [solver] = onchain.make_solvers(to_wei(200)).await;
let [trader] = onchain.make_accounts(to_wei(200)).await;
let [limit_order_token, market_order_token, partner_fee_order_token] = onchain
.deploy_tokens_with_weth_uni_v2_pools(to_wei(20), to_wei(20))
.await;
let [trader, trader_exempt] = onchain.make_accounts(to_wei(200)).await;
let [limit_order_token, market_order_token, partner_fee_order_token, fee_exempt_token] =
onchain
.deploy_tokens_with_weth_uni_v2_pools(to_wei(20), to_wei(20))
.await;

for token in &[
&limit_order_token,
&market_order_token,
&partner_fee_order_token,
&fee_exempt_token,
] {
token.mint(solver.address(), to_wei(1000)).await;
tx!(
Expand All @@ -82,24 +84,28 @@ async fn combined_protocol_fees(web3: Web3) {
to_wei(1000)
)
);
for trader in &[&trader, &trader_exempt] {
tx!(
trader.account(),
token.approve(onchain.contracts().uniswap_v2_router.address(), to_wei(100))
);
}
}

for trader in &[&trader, &trader_exempt] {
tx!(
trader.account(),
token.approve(onchain.contracts().uniswap_v2_router.address(), to_wei(100))
onchain
.contracts()
.weth
.approve(onchain.contracts().allowance, to_wei(100))
);
tx_value!(
trader.account(),
to_wei(100),
onchain.contracts().weth.deposit()
);
}

tx!(
trader.account(),
onchain
.contracts()
.weth
.approve(onchain.contracts().allowance, to_wei(100))
);
tx_value!(
trader.account(),
to_wei(100),
onchain.contracts().weth.deposit()
);
tx!(
solver.account(),
onchain
Expand All @@ -111,6 +117,10 @@ async fn combined_protocol_fees(web3: Web3) {
let autopilot_config = vec![
ProtocolFeesConfig(vec![limit_surplus_policy, market_price_improvement_policy]).to_string(),
"--fee-policy-max-partner-fee=0.02".to_string(),
format!(
"--protocol-fee-exempt-addresses={:?}",
trader_exempt.address()
),
];
let services = Services::new(onchain.contracts()).await;
services
Expand All @@ -126,12 +136,13 @@ async fn combined_protocol_fees(web3: Web3) {
tracing::info!("Acquiring quotes.");
let quote_valid_to = model::time::now_in_epoch_seconds() + 300;
let sell_amount = to_wei(10);
let [limit_quote_before, market_quote_before, partner_fee_quote] =
let [limit_quote_before, market_quote_before, partner_fee_quote, fee_exempt_quote] =
futures::future::try_join_all(
[
&limit_order_token,
&market_order_token,
&partner_fee_order_token,
&fee_exempt_token,
]
.map(|token| {
get_quote(
Expand All @@ -147,7 +158,7 @@ async fn combined_protocol_fees(web3: Web3) {
.await
.unwrap()
.try_into()
.expect("Expected exactly three elements");
.expect("Expected exactly four elements");

let market_price_improvement_order = OrderCreation {
sell_amount,
Expand Down Expand Up @@ -182,6 +193,16 @@ async fn combined_protocol_fees(web3: Web3) {
&onchain.contracts().domain_separator,
SecretKeyRef::from(&SecretKey::from_slice(trader.private_key()).unwrap()),
);
let fee_exempt_token_order = OrderCreation {
sell_amount,
buy_amount: to_wei(5),
..sell_order_from_quote(&fee_exempt_quote)
}
.sign(
EcdsaSigningScheme::Eip712,
&onchain.contracts().domain_separator,
SecretKeyRef::from(&SecretKey::from_slice(trader_exempt.private_key()).unwrap()),
);

tracing::info!("Rebalancing AMM pools for market & limit order.");
onchain
Expand Down Expand Up @@ -226,19 +247,20 @@ async fn combined_protocol_fees(web3: Web3) {
.try_into()
.expect("Expected exactly two elements");

let [market_price_improvement_uid, limit_surplus_order_uid, partner_fee_order_uid] =
let [market_price_improvement_uid, limit_surplus_order_uid, partner_fee_order_uid, fee_exempt_token_order_uid] =
futures::future::try_join_all(
[
&market_price_improvement_order,
&limit_surplus_order,
&partner_fee_order,
&fee_exempt_token_order,
]
.map(|order| services.create_order(order)),
)
.await
.unwrap()
.try_into()
.expect("Expected exactly three elements");
.expect("Expected exactly four elements");

tracing::info!("Waiting for orders to trade.");
let metadata_updated = || async {
Expand All @@ -248,6 +270,7 @@ async fn combined_protocol_fees(web3: Web3) {
&market_price_improvement_uid,
&limit_surplus_order_uid,
&partner_fee_order_uid,
&fee_exempt_token_order_uid,
]
.map(|uid| async {
services
Expand Down Expand Up @@ -295,6 +318,13 @@ async fn combined_protocol_fees(web3: Web3) {
// see `limit_surplus_policy.factor`, which is 0.3
assert!(limit_executed_surplus_fee_in_buy_token >= limit_quote_diff * 3 / 10);

let fee_exempt_order = services
.get_order(&fee_exempt_token_order_uid)
.await
.unwrap();
let fee_exempt_surplus_fee_in_buy_token =
surplus_fee_in_buy_token(&fee_exempt_order, &fee_exempt_quote.quote);

let balance_after = market_order_token
.balance_of(onchain.contracts().gp_settlement.address())
.call()
Expand All @@ -315,6 +345,13 @@ async fn combined_protocol_fees(web3: Web3) {
.await
.unwrap();
assert_approximately_eq!(partner_fee_executed_surplus_fee_in_buy_token, balance_after);

let balance_after = fee_exempt_token
.balance_of(onchain.contracts().gp_settlement.address())
.call()
.await
.unwrap();
assert_approximately_eq!(fee_exempt_surplus_fee_in_buy_token, balance_after);
}

async fn get_quote(
Expand Down

0 comments on commit 6c17db6

Please sign in to comment.