Skip to content

Commit f1841f0

Browse files
squadgazzzCopilot
authored andcommitted
Protocol fee switch-over timestamp (cowprotocol#3907)
# Description cowprotocol#3900 introduced a way to show the volume fees to users in advance via quotes. Autopilot should start applying the same policies simultaneously to avoid situations where the user receives an incorrect quote. This PR introduces timestamp-based configs in both orderbook and autopilot crates that control when the volume fee should start applying. For the orderbook, it is pretty straightforward, which adds an optional timestamp param to the existing volume fee factor config, and each time the service tries to apply volume fees, it checks for the current time. If the timestamp config is None, it means volume fees are applied unconditionally, which should be useful once switched to a long-lasting config. The autopilot config is a bit more sophisticated. It introduces a separate "upcoming" fee policies config with the effective "from" timestamp. ~~So, if another order's creation timestamp is after the configured upcoming fee policy timestamp, the service starts using this fee policy.~~ Based on [this discussion](cowprotocol#3907 (comment)), the volume fee gets applied only based on the current time. This is useful because the volume fee factor affects price improvement and surplus fee policy caps, so each time the volume fee factor is updated, other fee policy configs need to be adjusted accordingly, so we need to switch to the new fee policies set altogether. The config is also optional and can be easily switched to permanent. The major disadvantage of this approach is that orderbook and autopilot use configs from different sources. A more correct approach would be to use a shared config via a DB or similar, but this would require many more changes, and we should probably avoid any mistakes by making deeper reviews. ## How to test New e2e tests. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 37de01d commit f1841f0

File tree

10 files changed

+425
-74
lines changed

10 files changed

+425
-74
lines changed

crates/autopilot/src/arguments.rs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use {
22
crate::{domain::fee::FeeFactor, infra},
33
alloy::primitives::Address,
44
anyhow::{Context, anyhow, ensure},
5+
chrono::{DateTime, Utc},
56
clap::ValueEnum,
67
primitive_types::{H160, U256},
78
shared::{
@@ -194,13 +195,8 @@ pub struct Arguments {
194195
pub solve_deadline: Duration,
195196

196197
/// Describes how the protocol fees should be calculated.
197-
#[clap(long, env, use_value_delimiter = true)]
198-
pub fee_policies: Vec<FeePolicy>,
199-
200-
/// Maximum partner fee allow. If the partner fee specified is greater than
201-
/// this maximum, the partner fee will be capped
202-
#[clap(long, env, default_value = "0.01")]
203-
pub fee_policy_max_partner_fee: FeeFactor,
198+
#[clap(flatten)]
199+
pub fee_policies_config: FeePoliciesConfig,
204200

205201
/// Arguments for uploading information to S3.
206202
#[clap(flatten)]
@@ -389,8 +385,7 @@ impl std::fmt::Display for Arguments {
389385
submission_deadline,
390386
shadow,
391387
solve_deadline,
392-
fee_policies,
393-
fee_policy_max_partner_fee,
388+
fee_policies_config,
394389
order_events_cleanup_interval,
395390
order_events_cleanup_threshold,
396391
db_write_url,
@@ -448,11 +443,7 @@ impl std::fmt::Display for Arguments {
448443
writeln!(f, "submission_deadline: {submission_deadline}")?;
449444
display_option(f, "shadow", shadow)?;
450445
writeln!(f, "solve_deadline: {solve_deadline:?}")?;
451-
writeln!(f, "fee_policies: {fee_policies:?}")?;
452-
writeln!(
453-
f,
454-
"fee_policy_max_partner_fee: {fee_policy_max_partner_fee:?}"
455-
)?;
446+
writeln!(f, "fee_policies_config: {fee_policies_config:?}")?;
456447
writeln!(
457448
f,
458449
"order_events_cleanup_interval: {order_events_cleanup_interval:?}"
@@ -585,6 +576,22 @@ impl FromStr for Solver {
585576
}
586577
}
587578

579+
#[derive(clap::Parser, Debug, Clone)]
580+
pub struct FeePoliciesConfig {
581+
/// Describes how the protocol fees should be calculated.
582+
#[clap(long, env, use_value_delimiter = true)]
583+
pub fee_policies: Vec<FeePolicy>,
584+
585+
/// Maximum partner fee allowed. If the partner fee specified is greater
586+
/// than this maximum, the partner fee will be capped
587+
#[clap(long, env, default_value = "0.01")]
588+
pub fee_policy_max_partner_fee: FeeFactor,
589+
590+
/// Volume fee policies that will become effective at a future timestamp.
591+
#[clap(flatten)]
592+
pub upcoming_fee_policies: UpcomingFeePolicies,
593+
}
594+
588595
/// A fee policy to be used for orders base on it's class.
589596
/// Examples:
590597
/// - Surplus with a high enough cap for limit orders: surplus:0.5:0.9:limit
@@ -604,6 +611,24 @@ pub struct FeePolicy {
604611
pub fee_policy_order_class: FeePolicyOrderClass,
605612
}
606613

614+
/// Fee policies that will become effective at a future timestamp.
615+
#[derive(clap::Parser, Debug, Clone)]
616+
pub struct UpcomingFeePolicies {
617+
#[clap(
618+
id = "upcoming_fee_policies",
619+
long = "upcoming-fee-policies",
620+
env = "UPCOMING_FEE_POLICIES",
621+
use_value_delimiter = true
622+
)]
623+
pub fee_policies: Vec<FeePolicy>,
624+
625+
#[clap(
626+
long = "upcoming-fee-policies-timestamp",
627+
env = "UPCOMING_FEE_POLICIES_TIMESTAMP"
628+
)]
629+
pub effective_from_timestamp: Option<DateTime<Utc>>,
630+
}
631+
607632
#[derive(clap::Parser, Debug, Clone)]
608633
pub enum FeePolicyKind {
609634
/// How much of the order's surplus should be taken as a protocol fee.

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

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use {
1414
},
1515
alloy::primitives::{Address, U256},
1616
app_data::Validator,
17+
chrono::{DateTime, Utc},
1718
derive_more::Into,
1819
ethrpc::alloy::conversions::{IntoAlloy, IntoLegacy},
1920
primitive_types::H160,
@@ -53,25 +54,47 @@ impl From<arguments::FeePolicy> for ProtocolFee {
5354
}
5455
}
5556

57+
pub struct UpcomingProtocolFees {
58+
fee_policies: Vec<ProtocolFee>,
59+
effective_from_timestamp: DateTime<Utc>,
60+
}
61+
62+
impl From<arguments::UpcomingFeePolicies> for Option<UpcomingProtocolFees> {
63+
fn from(value: arguments::UpcomingFeePolicies) -> Self {
64+
value
65+
// both config fields must be non-empty
66+
.effective_from_timestamp
67+
.filter(|_| !value.fee_policies.is_empty())
68+
.map(|effective_from_timestamp| UpcomingProtocolFees {
69+
fee_policies: value
70+
.fee_policies
71+
.into_iter()
72+
.map(ProtocolFee::from)
73+
.collect::<Vec<_>>(),
74+
effective_from_timestamp,
75+
})
76+
}
77+
}
78+
5679
pub type ProtocolFeeExemptAddresses = HashSet<H160>;
5780

5881
pub struct ProtocolFees {
5982
fee_policies: Vec<ProtocolFee>,
6083
max_partner_fee: FeeFactor,
84+
upcoming_fee_policies: Option<UpcomingProtocolFees>,
6185
}
6286

6387
impl ProtocolFees {
64-
pub fn new(
65-
fee_policies: &[arguments::FeePolicy],
66-
fee_policy_max_partner_fee: FeeFactor,
67-
) -> Self {
88+
pub fn new(config: &arguments::FeePoliciesConfig) -> Self {
6889
Self {
69-
fee_policies: fee_policies
90+
fee_policies: config
91+
.fee_policies
7092
.iter()
7193
.cloned()
7294
.map(ProtocolFee::from)
7395
.collect(),
74-
max_partner_fee: fee_policy_max_partner_fee,
96+
max_partner_fee: config.fee_policy_max_partner_fee,
97+
upcoming_fee_policies: config.upcoming_fee_policies.clone().into(),
7598
}
7699
}
77100

@@ -230,13 +253,21 @@ impl ProtocolFees {
230253
quote: domain::Quote,
231254
partner_fees: Vec<Policy>,
232255
) -> domain::Order {
233-
let protocol_fees = self
234-
.fee_policies
256+
let now = Utc::now();
257+
let fee_policies = self
258+
.upcoming_fee_policies
259+
.as_ref()
260+
.filter(|upcoming| upcoming.effective_from_timestamp <= now)
261+
.map(|upcoming| &upcoming.fee_policies)
262+
.unwrap_or(&self.fee_policies);
263+
264+
let protocol_fees = fee_policies
235265
.iter()
236266
.filter_map(|fee_policy| Self::protocol_fee_into_policy(&order, &quote, fee_policy))
237267
.flat_map(|policy| Self::variant_fee_apply(&order, &quote, policy))
238268
.chain(partner_fees)
239269
.collect::<Vec<_>>();
270+
240271
boundary::order::to_domain(order, protocol_fees, Some(quote))
241272
}
242273

crates/autopilot/src/run.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ pub async fn run(args: Arguments, shutdown_controller: ShutdownController) {
538538
args.limit_order_price_factor
539539
.try_into()
540540
.expect("limit order price factor can't be converted to BigDecimal"),
541-
domain::ProtocolFees::new(&args.fee_policies, args.fee_policy_max_partner_fee),
541+
domain::ProtocolFees::new(&args.fee_policies_config),
542542
cow_amm_registry.clone(),
543543
args.run_loop_native_price_timeout,
544544
eth.contracts().settlement().address().into_legacy(),

crates/e2e/src/setup/fee.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
pub struct ProtocolFeesConfig(pub Vec<ProtocolFee>);
1+
use chrono::{DateTime, Utc};
2+
3+
#[derive(Default)]
4+
pub struct ProtocolFeesConfig {
5+
pub protocol_fees: Vec<ProtocolFee>,
6+
pub upcoming_protocol_fees: Option<UpcomingProtocolFees>,
7+
}
8+
9+
#[derive(Clone)]
10+
pub struct UpcomingProtocolFees {
11+
pub fee_policies: Vec<ProtocolFee>,
12+
pub effective_from_timestamp: DateTime<Utc>,
13+
}
214

315
#[derive(Clone)]
416
pub struct ProtocolFee {
@@ -57,14 +69,31 @@ impl std::fmt::Display for ProtocolFee {
5769
}
5870
}
5971

60-
impl std::fmt::Display for ProtocolFeesConfig {
61-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72+
impl ProtocolFeesConfig {
73+
pub fn into_args(self) -> Vec<String> {
74+
let mut args = Vec::new();
6275
let fees_str = self
63-
.0
76+
.protocol_fees
6477
.iter()
6578
.map(|fee| fee.to_string())
6679
.collect::<Vec<_>>()
6780
.join(",");
68-
write!(f, "--fee-policies={fees_str}")
81+
args.push(format!("--fee-policies={fees_str}"));
82+
83+
if let Some(upcoming_protocol_fees) = &self.upcoming_protocol_fees {
84+
let upcoming_fees_str = upcoming_protocol_fees
85+
.fee_policies
86+
.iter()
87+
.map(|fee| fee.to_string())
88+
.collect::<Vec<_>>()
89+
.join(",");
90+
args.push(format!("--upcoming-fee-policies={}", upcoming_fees_str));
91+
args.push(format!(
92+
"--upcoming-fee-policies-timestamp={}",
93+
upcoming_protocol_fees.effective_from_timestamp.to_rfc3339()
94+
));
95+
}
96+
97+
args
6998
}
7099
}

crates/e2e/tests/e2e/limit_orders.rs

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,32 +1066,36 @@ async fn no_liquidity_limit_order(web3: Web3) {
10661066
.unwrap();
10671067

10681068
// Setup services
1069-
let protocol_fees_config = ProtocolFeesConfig(vec![
1070-
ProtocolFee {
1071-
policy: fee::FeePolicyKind::Surplus {
1072-
factor: 0.5,
1073-
max_volume_factor: 0.01,
1069+
let protocol_fee_args = ProtocolFeesConfig {
1070+
protocol_fees: vec![
1071+
ProtocolFee {
1072+
policy: fee::FeePolicyKind::Surplus {
1073+
factor: 0.5,
1074+
max_volume_factor: 0.01,
1075+
},
1076+
policy_order_class: FeePolicyOrderClass::Limit,
10741077
},
1075-
policy_order_class: FeePolicyOrderClass::Limit,
1076-
},
1077-
ProtocolFee {
1078-
policy: fee::FeePolicyKind::PriceImprovement {
1079-
factor: 0.5,
1080-
max_volume_factor: 0.01,
1078+
ProtocolFee {
1079+
policy: fee::FeePolicyKind::PriceImprovement {
1080+
factor: 0.5,
1081+
max_volume_factor: 0.01,
1082+
},
1083+
policy_order_class: FeePolicyOrderClass::Market,
10811084
},
1082-
policy_order_class: FeePolicyOrderClass::Market,
1083-
},
1084-
])
1085-
.to_string();
1085+
],
1086+
..Default::default()
1087+
}
1088+
.into_args();
10861089

10871090
let services = Services::new(&onchain).await;
10881091
services
10891092
.start_protocol_with_args(
10901093
ExtraServiceArgs {
1091-
autopilot: vec![
1092-
protocol_fees_config,
1093-
format!("--unsupported-tokens={:#x}", unsupported.address()),
1094-
],
1094+
autopilot: [
1095+
protocol_fee_args,
1096+
vec![format!("--unsupported-tokens={:#x}", unsupported.address())],
1097+
]
1098+
.concat(),
10951099
..Default::default()
10961100
},
10971101
solver,

0 commit comments

Comments
 (0)