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

Make the partner fee max cap configurable in autopilot #2559

Merged
merged 1 commit into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions crates/autopilot/src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ pub struct Arguments {
#[clap(long, env, use_value_delimiter = true)]
pub fee_policies: Vec<FeePolicy>,

/// Maximum partner fee allow. If the partner fee specified is greater than
/// this maximum, the partner fee will be capped
#[clap(long, env, default_value = "0.01")]
pub fee_policy_max_partner_fee: FeeFactor,

/// Arguments for uploading information to S3.
#[clap(flatten)]
pub s3: infra::persistence::cli::S3,
Expand Down Expand Up @@ -253,6 +258,7 @@ impl std::fmt::Display for Arguments {
shadow,
solve_deadline,
fee_policies,
fee_policy_max_partner_fee,
order_events_cleanup_interval,
order_events_cleanup_threshold,
db_url,
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,
"fee_policy_max_partner_fee: {:?}",
fee_policy_max_partner_fee
)?;
writeln!(
f,
"order_events_cleanup_interval: {:?}",
Expand Down
46 changes: 35 additions & 11 deletions crates/autopilot/src/domain/fee/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use {
crate::{
arguments,
boundary::{self},
domain,
domain::{self},
},
app_data::Validator,
derive_more::Into,
Expand Down Expand Up @@ -42,18 +42,39 @@ pub struct ProtocolFee {
order_class: OrderClass,
}

impl ProtocolFee {
pub fn new(fee_policy_args: arguments::FeePolicy) -> Self {
impl From<arguments::FeePolicy> for ProtocolFee {
fn from(value: arguments::FeePolicy) -> Self {
Self {
policy: fee_policy_args.fee_policy_kind.into(),
order_class: fee_policy_args.fee_policy_order_class.into(),
policy: value.fee_policy_kind.into(),
order_class: value.fee_policy_order_class.into(),
}
}
}

pub struct ProtocolFees {
fee_policies: Vec<ProtocolFee>,
max_partner_fee: FeeFactor,
}

impl ProtocolFees {
pub fn new(
fee_policies: &[arguments::FeePolicy],
fee_policy_max_partner_fee: FeeFactor,
) -> Self {
Self {
fee_policies: fee_policies
.iter()
.cloned()
.map(ProtocolFee::from)
.collect(),
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: &[ProtocolFee],
protocol_fees: &ProtocolFees,
order: boundary::Order,
quote: &domain::Quote,
) -> domain::Order {
Expand All @@ -69,9 +90,11 @@ impl ProtocolFee {
{
if let Some(partner_fee) = validated_app_data.protocol.partner_fee {
let fee_policy = vec![Policy::Volume {
factor: FeeFactor::partner_fee_capped_from(
factor: FeeFactor::try_from_capped(
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to double check. The fee policy extracted from the app data here will get stored in the DB when the order gets executed during an auction, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a really good question! @sunce86 correct me if I am wrong, but as far as I understood it, by placing the code there (domain/fee/mod.rs), the action is cached and then taken as the next auction to process and find a solution in the single_run() (run_loop.rs) it is stored the order event.

partner_fee.bps.into_f64() / 10_000.0,
),
protocol_fees.max_partner_fee.into(),
)
.unwrap(),
}];
return boundary::order::to_domain(order, fee_policy);
}
Expand All @@ -88,6 +111,7 @@ impl ProtocolFee {
fee: quote.fee,
};
let protocol_fees = protocol_fees
.fee_policies
.iter()
// TODO: support multiple fee policies
.find_map(|fee_policy| {
Expand Down Expand Up @@ -149,9 +173,9 @@ pub enum Policy {
pub struct FeeFactor(f64);

impl FeeFactor {
/// Convert a partner fee into a `Factor` capping its value
pub fn partner_fee_capped_from(value: f64) -> Self {
Self(value.max(0.0).min(0.01))
/// Convert a fee into a `FeeFactor` capping its value
pub fn try_from_capped(value: f64, cap: f64) -> anyhow::Result<Self> {
value.max(0.0).min(cap).try_into()
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/autopilot/src/domain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ pub use {
Auction,
AuctionWithId,
},
fee::ProtocolFee,
fee::ProtocolFees,
quote::Quote,
};
5 changes: 1 addition & 4 deletions crates/autopilot/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,10 +560,7 @@ pub async fn run(args: Arguments) {
args.limit_order_price_factor
.try_into()
.expect("limit order price factor can't be converted to BigDecimal"),
args.fee_policies
.into_iter()
.map(domain::ProtocolFee::new)
.collect(),
domain::ProtocolFees::new(&args.fee_policies, args.fee_policy_max_partner_fee),
);
solvable_orders_cache
.update(block)
Expand Down
6 changes: 3 additions & 3 deletions crates/autopilot/src/solvable_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub struct SolvableOrdersCache {
metrics: &'static Metrics,
weth: H160,
limit_order_price_factor: BigDecimal,
protocol_fees: Vec<domain::ProtocolFee>,
protocol_fees: domain::ProtocolFees,
}

type Balances = HashMap<Query, U256>;
Expand All @@ -104,7 +104,7 @@ impl SolvableOrdersCache {
update_interval: Duration,
weth: H160,
limit_order_price_factor: BigDecimal,
protocol_fees: Vec<domain::ProtocolFee>,
protocol_fees: domain::ProtocolFees,
) -> Arc<Self> {
let self_ = Arc::new(Self {
min_order_validity_period,
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::ProtocolFee::apply(&self.protocol_fees, order, quote))
Some(domain::ProtocolFees::apply(&self.protocol_fees, order, quote))
} else {
tracing::warn!(order_uid = %order.metadata.uid, "order is skipped, quote is missing");
None
Expand Down
79 changes: 67 additions & 12 deletions crates/e2e/tests/e2e/protocol_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ async fn local_node_partner_fee_sell_order() {
run_test(partner_fee_sell_order_test).await;
}

#[tokio::test]
#[ignore]
async fn local_node_partner_fee_with_not_default_cap_sell_order_test() {
run_test(partner_fee_with_not_default_cap_sell_order_test).await;
}

#[tokio::test]
#[ignore]
async fn local_node_surplus_fee_buy_order() {
Expand Down Expand Up @@ -91,7 +97,7 @@ async fn surplus_fee_sell_order_test(web3: Web3) {
// 1480603400674076736) = 1461589542731026166 DAI
execute_test(
web3.clone(),
vec![protocol_fee],
vec![ProtocolFeesConfig(vec![protocol_fee])],
OrderKind::Sell,
None,
1480603400674076736u128.into(),
Expand Down Expand Up @@ -133,7 +139,7 @@ async fn surplus_fee_sell_order_capped_test(web3: Web3) {
// 1000150353094783059) = 987306456662572858 DAI
execute_test(
web3.clone(),
protocol_fees,
vec![ProtocolFeesConfig(protocol_fees)],
OrderKind::Sell,
None,
1000150353094783059u128.into(),
Expand Down Expand Up @@ -166,7 +172,7 @@ async fn volume_fee_sell_order_test(web3: Web3) {
// 1000150353094783059) = 987306456662572858 DAI
execute_test(
web3.clone(),
vec![protocol_fee],
vec![ProtocolFeesConfig(vec![protocol_fee])],
OrderKind::Sell,
None,
1000150353094783059u128.into(),
Expand Down Expand Up @@ -201,7 +207,7 @@ async fn partner_fee_sell_order_test(web3: Web3) {
// 100165388404261365) = 98879067931768848 DAI
execute_test(
web3.clone(),
vec![protocol_fee],
vec![ProtocolFeesConfig(vec![protocol_fee])],
OrderKind::Sell,
Some(OrderCreationAppData::Full {
full: json!({
Expand All @@ -221,6 +227,55 @@ async fn partner_fee_sell_order_test(web3: Web3) {
.await;
}

async fn partner_fee_with_not_default_cap_sell_order_test(web3: Web3) {
// Fee policy to be overwritten by the partner fee + capped to 0.02
let fee_policy = FeePolicyKind::PriceImprovement {
factor: 0.5,
max_volume_factor: 0.9,
};
let protocol_fee = ProtocolFee {
policy: fee_policy,
policy_order_class: FeePolicyOrderClass::Market,
};
// Without protocol fee:
// Expected execution is 10000000000000000000 GNO for
// 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO
//
// With protocol fee:
// Expected executed_surplus_fee is 167058994203399 +
// 0.02*(10000000000000000000 - 167058994203399) = 200163717814319331
//
// Final execution is 10000000000000000000 GNO for 9772701276038844388 DAI, with
// executed_surplus_fee = 200163717814319331 GNO
//
// Settlement contract balance after execution = 200163717814319331 GNO =
// 200163717814319331 GNO * 9673987121735421787 / (10000000000000000000 -
// 200163717814319331) = 197593222235191520 DAI
execute_test(
web3.clone(),
vec![
ProtocolFeesConfig(vec![protocol_fee]).to_string(),
"--fee-policy-max-partner-fee=0.02".to_string(),
],
OrderKind::Sell,
Some(OrderCreationAppData::Full {
full: json!({
"version": "1.1.0",
"metadata": {
"partnerFee": {
"bps":1000,
"recipient": "0xb6BAd41ae76A11D10f7b0E664C5007b908bC77C9",
}
}
})
.to_string(),
}),
200163717814319331u128.into(),
197593222235191520u128.into(),
)
.await;
}

async fn surplus_fee_buy_order_test(web3: Web3) {
let fee_policy = FeePolicyKind::Surplus {
factor: 0.3,
Expand All @@ -246,7 +301,7 @@ async fn surplus_fee_buy_order_test(web3: Web3) {
// Settlement contract balance after execution = executed_surplus_fee GNO
execute_test(
web3.clone(),
vec![protocol_fee],
vec![ProtocolFeesConfig(vec![protocol_fee])],
OrderKind::Buy,
None,
1488043031123213136u128.into(),
Expand Down Expand Up @@ -275,7 +330,7 @@ async fn surplus_fee_buy_order_capped_test(web3: Web3) {
// Settlement contract balance after execution = executed_surplus_fee GNO
execute_test(
web3.clone(),
vec![protocol_fee],
vec![ProtocolFeesConfig(vec![protocol_fee])],
OrderKind::Buy,
None,
504208401617866820u128.into(),
Expand All @@ -301,7 +356,7 @@ async fn volume_fee_buy_order_test(web3: Web3) {
// Settlement contract balance after execution = executed_surplus_fee GNO
execute_test(
web3.clone(),
vec![protocol_fee],
vec![ProtocolFeesConfig(vec![protocol_fee])],
OrderKind::Buy,
None,
504208401617866820u128.into(),
Expand Down Expand Up @@ -343,7 +398,7 @@ async fn price_improvement_fee_sell_order_test(web3: Web3) {
// 205312824093583) = 202676203868731 DAI
execute_test(
web3.clone(),
vec![protocol_fee],
vec![ProtocolFeesConfig(vec![protocol_fee])],
OrderKind::Sell,
None,
205312824093583u128.into(),
Expand All @@ -362,7 +417,7 @@ fn is_approximately_equal(executed_value: U256, expected_value: U256) -> bool {

async fn execute_test(
web3: Web3,
protocol_fees: Vec<ProtocolFee>,
autopilot_config: Vec<impl ToString>,
order_kind: OrderKind,
app_data: Option<OrderCreationAppData>,
expected_surplus_fee: U256,
Expand Down Expand Up @@ -435,12 +490,12 @@ async fn execute_test(
endpoint: solver_endpoint,
}],
);
let autopilot_args = vec![
let mut config = vec![
"--drivers=test_solver|http://localhost:11088/test_solver".to_string(),
"--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(),
ProtocolFeesConfig(protocol_fees).to_string(),
];
services.start_autopilot(None, autopilot_args);
config.extend(autopilot_config.iter().map(ToString::to_string));
services.start_autopilot(None, config);
services
.start_api(vec![
"--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(),
Expand Down
Loading