Skip to content

Commit ef17a28

Browse files
fafkMartinquaXD
andauthored
Add volume fee overrides (#3976)
# Description Implements volume fee bucket overrides, allowing custom volume fees for groups of tokens. This enables us to have different fees for a chosen set of tokens, e.g. stable coins. # Changes <!-- List of detailed changes (how the change is accomplished) --> - [x] Added `--volume-fee-bucket-overrides` CLI argument - Format: `"factor:token1,token2,..."` (e.g., `"0:0xA0b86...,0x6B175...,0xdAC17..."`) - Multiple buckets separated by semicolons - [x] Both buy and sell tokens must be in the same bucket for the override to apply - [x] Adjusts quote (orderbook) and order (autopilot) - [x] For buy token=sell token no fee applies ## How to test ``` cargo test --package e2e --test e2e protocol_fee::local_node_volume_fee_overrides ``` --------- Co-authored-by: Martin Magnus <[email protected]>
1 parent 760b024 commit ef17a28

File tree

15 files changed

+787
-142
lines changed

15 files changed

+787
-142
lines changed

crates/autopilot/src/arguments.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use {
2-
crate::{domain::fee::FeeFactor, infra},
2+
crate::infra,
33
alloy::primitives::{Address, U256},
44
anyhow::{Context, anyhow, ensure},
55
chrono::{DateTime, Utc},
66
clap::ValueEnum,
77
shared::{
8-
arguments::{display_list, display_option, display_secret_option},
8+
arguments::{FeeFactor, display_list, display_option, display_secret_option},
99
bad_token::token_owner_finder,
1010
http_client,
1111
price_estimation::{self, NativePriceEstimators},

crates/autopilot/src/domain/fee/mod.rs

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ use {
1515
alloy::primitives::{Address, U256},
1616
app_data::Validator,
1717
chrono::{DateTime, Utc},
18-
derive_more::Into,
1918
rust_decimal::Decimal,
20-
std::{collections::HashSet, str::FromStr},
19+
shared::{
20+
arguments::{FeeFactor, TokenBucketFeeOverride},
21+
fee::VolumeFeePolicy,
22+
},
23+
std::collections::HashSet,
2124
};
2225

2326
#[derive(Debug)]
@@ -80,10 +83,20 @@ pub struct ProtocolFees {
8083
fee_policies: Vec<ProtocolFee>,
8184
max_partner_fee: FeeFactor,
8285
upcoming_fee_policies: Option<UpcomingProtocolFees>,
86+
volume_fee_policy: VolumeFeePolicy,
8387
}
8488

8589
impl ProtocolFees {
86-
pub fn new(config: &arguments::FeePoliciesConfig) -> Self {
90+
pub fn new(
91+
config: &arguments::FeePoliciesConfig,
92+
volume_fee_bucket_overrides: Vec<TokenBucketFeeOverride>,
93+
enable_sell_equals_buy_volume_fee: bool,
94+
) -> Self {
95+
let volume_fee_policy = VolumeFeePolicy::new(
96+
volume_fee_bucket_overrides,
97+
None, // contained within FeePoliciesConfig; vol fee is passed in at callsite
98+
enable_sell_equals_buy_volume_fee,
99+
);
87100
Self {
88101
fee_policies: config
89102
.fee_policies
@@ -93,6 +106,7 @@ impl ProtocolFees {
93106
.collect(),
94107
max_partner_fee: config.fee_policy_max_partner_fee,
95108
upcoming_fee_policies: config.upcoming_fee_policies.clone().into(),
109+
volume_fee_policy,
96110
}
97111
}
98112

@@ -134,7 +148,7 @@ impl ProtocolFees {
134148
// update the `accumulated` value
135149
*accumulated += value.min(cap - *accumulated);
136150

137-
FeeFactor(f64::try_from(value.max(Decimal::ZERO).min(remaining_factor)).unwrap())
151+
FeeFactor::new(f64::try_from(value.max(Decimal::ZERO).min(remaining_factor)).unwrap())
138152
}
139153

140154
fn fee_factor_from_bps(bps: u64) -> FeeFactor {
@@ -236,7 +250,7 @@ impl ProtocolFees {
236250
});
237251

238252
let partner_fee =
239-
Self::get_partner_fee(&order, &reference_quote, self.max_partner_fee.into());
253+
Self::get_partner_fee(&order, &reference_quote, self.max_partner_fee.get());
240254

241255
if surplus_capturing_jit_order_owners.contains(&order.metadata.owner) {
242256
return boundary::order::to_domain(order, partner_fee, quote);
@@ -262,22 +276,23 @@ impl ProtocolFees {
262276
let protocol_fees = fee_policies
263277
.iter()
264278
.filter_map(|fee_policy| Self::protocol_fee_into_policy(&order, &quote, fee_policy))
265-
.flat_map(|policy| Self::variant_fee_apply(&order, &quote, policy))
279+
.flat_map(|policy| self.variant_fee_apply(&order, &quote, policy))
266280
.chain(partner_fees)
267281
.collect::<Vec<_>>();
268282

269283
boundary::order::to_domain(order, protocol_fees, Some(quote))
270284
}
271285

272286
fn variant_fee_apply(
287+
&self,
273288
order: &boundary::Order,
274289
quote: &domain::Quote,
275290
policy: &policy::Policy,
276291
) -> Option<Policy> {
277292
match policy {
278293
policy::Policy::Surplus(variant) => variant.apply(order),
279294
policy::Policy::PriceImprovement(variant) => variant.apply(order, quote),
280-
policy::Policy::Volume(variant) => variant.apply(order),
295+
policy::Policy::Volume(variant) => variant.apply(order, &self.volume_fee_policy),
281296
}
282297
}
283298

@@ -332,30 +347,6 @@ pub enum Policy {
332347
},
333348
}
334349

335-
#[derive(Debug, Clone, Copy, PartialEq, Into)]
336-
pub struct FeeFactor(f64);
337-
338-
/// TryFrom implementation for the cases we want to enforce the constrain [0, 1)
339-
impl TryFrom<f64> for FeeFactor {
340-
type Error = anyhow::Error;
341-
342-
fn try_from(value: f64) -> Result<Self, Self::Error> {
343-
anyhow::ensure!(
344-
(0.0..1.0).contains(&value),
345-
"Factor must be in the range [0, 1)"
346-
);
347-
Ok(FeeFactor(value))
348-
}
349-
}
350-
351-
impl FromStr for FeeFactor {
352-
type Err = anyhow::Error;
353-
354-
fn from_str(s: &str) -> Result<Self, Self::Err> {
355-
s.parse::<f64>().map(FeeFactor::try_from)?
356-
}
357-
}
358-
359350
#[derive(Debug, Copy, Clone, PartialEq)]
360351
pub struct Quote {
361352
/// The amount of the sell token.
@@ -423,10 +414,10 @@ mod test {
423414
result,
424415
vec![
425416
Policy::Volume {
426-
factor: FeeFactor(0.05),
417+
factor: FeeFactor::try_from(0.05).unwrap(),
427418
},
428419
Policy::Volume {
429-
factor: FeeFactor(0.2),
420+
factor: FeeFactor::try_from(0.2).unwrap(),
430421
}
431422
]
432423
);
@@ -497,7 +488,7 @@ mod test {
497488
assert_eq!(
498489
result,
499490
vec![Policy::Volume {
500-
factor: FeeFactor(0.0),
491+
factor: FeeFactor::try_from(0.0).unwrap(),
501492
}]
502493
);
503494
}
@@ -542,10 +533,10 @@ mod test {
542533
result,
543534
vec![
544535
Policy::Volume {
545-
factor: FeeFactor(0.0),
536+
factor: FeeFactor::try_from(0.0).unwrap(),
546537
},
547538
Policy::Volume {
548-
factor: FeeFactor(0.0),
539+
factor: FeeFactor::try_from(0.0).unwrap(),
549540
}
550541
]
551542
);
@@ -586,7 +577,7 @@ mod test {
586577
assert_eq!(
587578
result,
588579
vec![Policy::Volume {
589-
factor: FeeFactor(0.3),
580+
factor: FeeFactor::try_from(0.3).unwrap(),
590581
}]
591582
);
592583
}
@@ -634,10 +625,10 @@ mod test {
634625
result,
635626
vec![
636627
Policy::Volume {
637-
factor: FeeFactor(0.1),
628+
factor: FeeFactor::try_from(0.1).unwrap(),
638629
},
639630
Policy::Volume {
640-
factor: FeeFactor(0.18181818181818182),
631+
factor: FeeFactor::try_from(0.18181818181818182).unwrap(),
641632
}
642633
]
643634
);
@@ -691,13 +682,13 @@ mod test {
691682
result,
692683
vec![
693684
Policy::Volume {
694-
factor: FeeFactor(0.1),
685+
factor: FeeFactor::try_from(0.1).unwrap(),
695686
},
696687
Policy::Volume {
697-
factor: FeeFactor(0.18181818181818182),
688+
factor: FeeFactor::try_from(0.18181818181818182).unwrap(),
698689
},
699690
Policy::Volume {
700-
factor: FeeFactor(0.0),
691+
factor: FeeFactor::try_from(0.0).unwrap(),
701692
}
702693
]
703694
);

crates/autopilot/src/domain/fee/policy.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use crate::{
2-
arguments,
3-
boundary,
4-
domain::{
5-
self,
6-
fee::{FeeFactor, Quote},
1+
use {
2+
crate::{
3+
arguments,
4+
boundary,
5+
domain::{self, fee::Quote},
76
},
7+
shared::{arguments::FeeFactor, fee::VolumeFeePolicy},
88
};
99

1010
pub enum Policy {
@@ -84,13 +84,24 @@ impl PriceImprovement {
8484
}
8585

8686
impl Volume {
87-
pub fn apply(&self, order: &boundary::Order) -> Option<domain::fee::Policy> {
87+
pub fn apply(
88+
&self,
89+
order: &boundary::Order,
90+
volume_fee_policy: &VolumeFeePolicy,
91+
) -> Option<domain::fee::Policy> {
8892
match order.metadata.class {
8993
boundary::OrderClass::Market => None,
9094
boundary::OrderClass::Liquidity => None,
91-
boundary::OrderClass::Limit => Some(domain::fee::Policy::Volume {
92-
factor: self.factor,
93-
}),
95+
boundary::OrderClass::Limit => {
96+
// Use shared function to determine applicable volume fee factor
97+
let factor = volume_fee_policy.get_applicable_volume_fee_factor(
98+
order.data.buy_token,
99+
order.data.sell_token,
100+
Some(self.factor),
101+
)?;
102+
103+
Some(domain::fee::Policy::Volume { factor })
104+
}
94105
}
95106
}
96107
}

crates/autopilot/src/domain/settlement/trade/math.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,8 @@ impl Trade {
321321
} => {
322322
let surplus = self.surplus_over_limit_price()?;
323323
std::cmp::min(
324-
self.surplus_fee(surplus, (*factor).into())?,
325-
self.volume_fee((*max_volume_factor).into())?,
324+
self.surplus_fee(surplus, (*factor).get())?,
325+
self.volume_fee((*max_volume_factor).get())?,
326326
)
327327
}
328328
fee::Policy::PriceImprovement {
@@ -332,11 +332,11 @@ impl Trade {
332332
} => {
333333
let price_improvement = self.price_improvement(quote)?;
334334
std::cmp::min(
335-
self.surplus_fee(price_improvement, (*factor).into())?,
336-
self.volume_fee((*max_volume_factor).into())?,
335+
self.surplus_fee(price_improvement, (*factor).get())?,
336+
self.volume_fee((*max_volume_factor).get())?,
337337
)
338338
}
339-
fee::Policy::Volume { factor } => self.volume_fee((*factor).into())?,
339+
fee::Policy::Volume { factor } => self.volume_fee((*factor).get())?,
340340
};
341341
Ok(fee)
342342
}

crates/autopilot/src/infra/persistence/dto/fee_policy.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ pub fn from_domain(
1717
auction_id,
1818
order_uid: boundary::database::byte_array::ByteArray(order_uid.0),
1919
kind: FeePolicyKind::Surplus,
20-
surplus_factor: Some(factor.into()),
21-
surplus_max_volume_factor: Some(max_volume_factor.into()),
20+
surplus_factor: Some(factor.get()),
21+
surplus_max_volume_factor: Some(max_volume_factor.get()),
2222
volume_factor: None,
2323
price_improvement_factor: None,
2424
price_improvement_max_volume_factor: None,
@@ -29,7 +29,7 @@ pub fn from_domain(
2929
kind: FeePolicyKind::Volume,
3030
surplus_factor: None,
3131
surplus_max_volume_factor: None,
32-
volume_factor: Some(factor.into()),
32+
volume_factor: Some(factor.get()),
3333
price_improvement_factor: None,
3434
price_improvement_max_volume_factor: None,
3535
},
@@ -44,8 +44,8 @@ pub fn from_domain(
4444
surplus_factor: None,
4545
surplus_max_volume_factor: None,
4646
volume_factor: None,
47-
price_improvement_factor: Some(factor.into()),
48-
price_improvement_max_volume_factor: Some(max_volume_factor.into()),
47+
price_improvement_factor: Some(factor.get()),
48+
price_improvement_max_volume_factor: Some(max_volume_factor.get()),
4949
},
5050
}
5151
}

crates/autopilot/src/infra/persistence/dto/order.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use {
22
crate::{
33
boundary::{self},
4-
domain::{self, OrderUid, eth, fee::FeeFactor},
4+
domain::{self, OrderUid, eth},
55
},
66
alloy::primitives::{Address, U256},
77
app_data::AppDataHash,
88
number::serialization::HexOrDecimalU256,
99
serde::{Deserialize, Serialize},
1010
serde_with::serde_as,
11+
shared::arguments::FeeFactor,
1112
};
1213

1314
#[serde_as]
@@ -277,16 +278,16 @@ impl FeePolicy {
277278
factor,
278279
max_volume_factor,
279280
} => Self::Surplus {
280-
factor: factor.into(),
281-
max_volume_factor: max_volume_factor.into(),
281+
factor: factor.get(),
282+
max_volume_factor: max_volume_factor.get(),
282283
},
283284
domain::fee::Policy::PriceImprovement {
284285
factor,
285286
max_volume_factor,
286287
quote,
287288
} => Self::PriceImprovement {
288-
factor: factor.into(),
289-
max_volume_factor: max_volume_factor.into(),
289+
factor: factor.get(),
290+
max_volume_factor: max_volume_factor.get(),
290291
quote: Quote {
291292
sell_amount: quote.sell_amount,
292293
buy_amount: quote.buy_amount,
@@ -295,7 +296,7 @@ impl FeePolicy {
295296
},
296297
},
297298
domain::fee::Policy::Volume { factor } => Self::Volume {
298-
factor: factor.into(),
299+
factor: factor.get(),
299300
},
300301
}
301302
}

crates/autopilot/src/run.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,11 @@ pub async fn run(args: Arguments, shutdown_controller: ShutdownController) {
518518
args.limit_order_price_factor
519519
.try_into()
520520
.expect("limit order price factor can't be converted to BigDecimal"),
521-
domain::ProtocolFees::new(&args.fee_policies_config),
521+
domain::ProtocolFees::new(
522+
&args.fee_policies_config,
523+
args.shared.volume_fee_bucket_overrides.clone(),
524+
args.shared.enable_sell_equals_buy_volume_fee,
525+
),
522526
cow_amm_registry.clone(),
523527
args.run_loop_native_price_timeout,
524528
*eth.contracts().settlement().address(),

0 commit comments

Comments
 (0)