From bc6df24df2b11726698ccf8380dfb372a404ceb4 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Mon, 25 Dec 2023 17:35:49 +0100 Subject: [PATCH 01/35] Protocol fee calculation --- .../src/domain/competition/solution/trade.rs | 128 +++++++++++++++++- .../driver/src/infra/solver/dto/solution.rs | 5 + 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 9810ce7232..383f7a290c 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -23,6 +23,7 @@ pub struct Fulfillment { /// order. executed: order::TargetAmount, fee: Fee, + protocol_fee: order::SellAmount, } impl Fulfillment { @@ -30,7 +31,119 @@ impl Fulfillment { order: competition::Order, executed: order::TargetAmount, fee: Fee, + uniform_sell_price: eth::U256, + uniform_buy_price: eth::U256, ) -> Result { + let protocol_fee = { + let surplus_fee = match fee { + Fee::Static => eth::U256::default(), + Fee::Dynamic(fee) => fee.0, + }; + + let mut protocol_fee = Default::default(); + for fee_policy in &order.fee_policies { + match fee_policy { + order::FeePolicy::PriceImprovement { + factor, + max_volume_factor, + } => { + let fee = match order.side { + order::Side::Buy => { + // Equal to full sell amount for FOK orders, otherwise scalled with + // executed amount for partially + // fillable orders + let limit_sell_amount = + order.sell.amount.0 * executed.0 / order.buy.amount.0; + // How much `sell_token` we need to sell to buy `executed` amount of + // `buy_token` + let executed_sell_amount = executed + .0 + .checked_mul(uniform_buy_price) + .ok_or(InvalidExecutedAmount)? + .checked_div(uniform_sell_price) + .ok_or(InvalidExecutedAmount)?; + // We have to sell slightly more `sell_token` to capture the + // `surplus_fee` + let executed_sell_amount_with_surplus_fee = executed_sell_amount + .checked_add(surplus_fee) + .ok_or(InvalidExecutedAmount)?; + // Sold exactly `executed_sell_amount_with_surplus_fee` while the + // limit price is + // `limit_sell_amount` Take protocol fee from the price + // improvement + let price_improvement_fee = limit_sell_amount + .checked_sub(executed_sell_amount_with_surplus_fee) + .ok_or(InvalidExecutedAmount)? + * (eth::U256::from_f64_lossy(factor * 100.)) + / 100; + let max_volume_fee = executed_sell_amount_with_surplus_fee + * (eth::U256::from_f64_lossy(max_volume_factor * 100.)) + / 100; + // take the smaller of the two + std::cmp::min(price_improvement_fee, max_volume_fee) + } + order::Side::Sell => { + let executed_sell_amount = executed + .0 + .checked_add(surplus_fee) + .ok_or(InvalidExecutedAmount)?; + + // Equal to full buy amount for FOK orders, otherwise scalled with + // executed amount for partially + // fillable orders + let limit_buy_amount = + order.buy.amount.0 * executed_sell_amount / order.sell.amount.0; + // How much `buy_token` we get for `executed_sell_amount` of + // `sell_token` + let executed_buy_amount = executed_sell_amount + .checked_mul(uniform_sell_price) + .ok_or(InvalidExecutedAmount)? + .checked_div(uniform_buy_price) + .ok_or(InvalidExecutedAmount)?; + // Bought exactly `executed_buy_amount` while the limit price is + // `limit_buy_amount` Take protocol fee from the price + // improvement + let price_improvement_fee = executed_buy_amount + .checked_sub(limit_buy_amount) + .ok_or(InvalidExecutedAmount)? + * (eth::U256::from_f64_lossy(factor * 100.)) + / 100; + let max_volume_fee = executed_buy_amount + * (eth::U256::from_f64_lossy(max_volume_factor * 100.)) + / 100; + // take the smaller of the two + let protocol_fee_in_buy_amount = + std::cmp::min(price_improvement_fee, max_volume_fee); + + // express protocol fee in sell token + protocol_fee_in_buy_amount + .checked_mul(uniform_buy_price) + .ok_or(InvalidExecutedAmount)? + .checked_div(uniform_sell_price) + .ok_or(InvalidExecutedAmount)? + } + }; + protocol_fee += fee; + } + order::FeePolicy::Volume { factor: _ } => unimplemented!(), + } + } + order::SellAmount(protocol_fee) + }; + + // Adjust the executed amount by the protocol fee. This is because solvers are + // unaware of the protocol fee that driver introduces and they only account + // for the network fee. + let executed = match order.side { + order::Side::Buy => executed, + order::Side::Sell => order::TargetAmount( + executed + .0 + .checked_sub(protocol_fee.0) + .ok_or(InvalidExecutedAmount)?, + ), + }; + // If the order is partial, the total executed amount can be smaller than // the target amount. Otherwise, the executed amount must be equal to the target // amount. @@ -43,12 +156,18 @@ impl Fulfillment { }), }; + let protocol_fee = match order.side { + order::Side::Buy => order::TargetAmount::default(), + order::Side::Sell => order::TargetAmount(protocol_fee.0), + }; + match order.partial { order::Partial::Yes { available } => { - order::TargetAmount(executed.0 + surplus_fee.0) <= available + order::TargetAmount(executed.0 + surplus_fee.0 + protocol_fee.0) <= available } order::Partial::No => { - order::TargetAmount(executed.0 + surplus_fee.0) == order.target() + order::TargetAmount(executed.0 + surplus_fee.0 + protocol_fee.0) + == order.target() } } }; @@ -65,6 +184,7 @@ impl Fulfillment { order, executed, fee, + protocol_fee, }) } else { Err(InvalidExecutedAmount) @@ -84,7 +204,7 @@ impl Fulfillment { pub fn scoring_fee(&self) -> order::SellAmount { match self.fee { Fee::Static => self.order.fee.solver, - Fee::Dynamic(fee) => fee, + Fee::Dynamic(fee) => (fee.0 + self.protocol_fee.0).into(), } } @@ -93,7 +213,7 @@ impl Fulfillment { pub fn fee(&self) -> order::SellAmount { match self.fee { Fee::Static => self.order.fee.user, - Fee::Dynamic(fee) => fee, + Fee::Dynamic(fee) => (fee.0 + self.protocol_fee.0).into(), } } diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index 8f9dd40ba8..e1ec782bd5 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -38,6 +38,9 @@ impl Solutions { ))? .clone(); + let uniform_sell_price = solution.prices[&order.sell.token.into()]; + let uniform_buy_price = solution.prices[&order.buy.token.into()]; + competition::solution::trade::Fulfillment::new( order, fulfillment.executed_amount.into(), @@ -47,6 +50,8 @@ impl Solutions { ), None => competition::solution::trade::Fee::Static, }, + uniform_sell_price, + uniform_buy_price, ) .map(competition::solution::Trade::Fulfillment) .map_err( From 36443b92ebc295d080bc40b7d173be536e41f0c3 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Tue, 26 Dec 2023 01:09:13 +0100 Subject: [PATCH 02/35] sell limit orders good --- .../src/domain/competition/solution/trade.rs | 114 +++++++++++++++--- 1 file changed, 97 insertions(+), 17 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 383f7a290c..9dc3ce2e61 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -87,40 +87,36 @@ impl Fulfillment { .0 .checked_add(surplus_fee) .ok_or(InvalidExecutedAmount)?; - // Equal to full buy amount for FOK orders, otherwise scalled with // executed amount for partially // fillable orders let limit_buy_amount = order.buy.amount.0 * executed_sell_amount / order.sell.amount.0; - // How much `buy_token` we get for `executed_sell_amount` of - // `sell_token` - let executed_buy_amount = executed_sell_amount + // How much `buy_token` we get for `executed` amount of `sell_token` + let executed_buy_amount = executed + .0 .checked_mul(uniform_sell_price) .ok_or(InvalidExecutedAmount)? .checked_div(uniform_buy_price) .ok_or(InvalidExecutedAmount)?; // Bought exactly `executed_buy_amount` while the limit price is - // `limit_buy_amount` Take protocol fee from the price - // improvement - let price_improvement_fee = executed_buy_amount + // `limit_buy_amount` Take protocol fee from the surplus + let surplus = executed_buy_amount .checked_sub(limit_buy_amount) + .ok_or(InvalidExecutedAmount)?; + let surplus_in_sell_token = surplus + .checked_mul(uniform_buy_price) .ok_or(InvalidExecutedAmount)? + .checked_div(uniform_sell_price) + .ok_or(InvalidExecutedAmount)?; + let price_improvement_fee = surplus_in_sell_token * (eth::U256::from_f64_lossy(factor * 100.)) / 100; - let max_volume_fee = executed_buy_amount + let max_volume_fee = executed_sell_amount * (eth::U256::from_f64_lossy(max_volume_factor * 100.)) / 100; // take the smaller of the two - let protocol_fee_in_buy_amount = - std::cmp::min(price_improvement_fee, max_volume_fee); - - // express protocol fee in sell token - protocol_fee_in_buy_amount - .checked_mul(uniform_buy_price) - .ok_or(InvalidExecutedAmount)? - .checked_div(uniform_sell_price) - .ok_or(InvalidExecutedAmount)? + std::cmp::min(price_improvement_fee, max_volume_fee) } }; protocol_fee += fee; @@ -323,3 +319,87 @@ pub enum ExecutionError { #[error("missing clearing price for {0:?}")] ClearingPriceMissing(eth::TokenAddress), } + +mod tests { + use { + super::*, + crate::{ + domain::competition::order::{ + signature::Scheme, + AppData, + BuyTokenBalance, + FeePolicy, + SellTokenBalance, + Signature, + }, + util, + }, + primitive_types::{H160, U256}, + std::str::FromStr, + }; + + #[test] + fn test_fulfillment() { + // https://explorer.cow.fi/orders/0xef6de27933bde867c768ead05d34a08c806d35b89f6bea565bdeb40108265e9a6f419390da10911abd1e1c962b569312a9c9c7b1658a2936?tab=overview + let order = competition::Order { + uid: Default::default(), + side: order::Side::Sell, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xba3335588d9403515223f109edc4eb7269a9ab5d").unwrap(), + )), + amount: eth::TokenAmount(778310860032541096349039u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), + )), + amount: eth::TokenAmount(4166666666666666666u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::No, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.25?id=m8dnoowB4Ql8nk7a5ber + let uniform_sell_price = eth::U256::from(913320970421237626580182u128); + let uniform_buy_price = eth::U256::from(4149866666666666668u128); + let executed = order::TargetAmount(4149866666666666668u128.into()); + let fee = Fee::Dynamic(order::SellAmount(16799999999999998u128.into())); + let fulfillment = Fulfillment::new( + order.clone(), + executed, + fee, + uniform_sell_price, + uniform_buy_price, + ) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((16799999999999998u128 + 306723471216604081u128).into()) + ); + println!("{:?}", fulfillment.fee()); + // executed amount reduced by protocol fee + assert_eq!( + fulfillment.executed(), + U256::from(3843143195450062587u128).into() + ); // 4149866666666666668 - 306723471216604081 + } +} From 5acaaf30b54a6885be6f1ca367f4cf1f41729bb1 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Tue, 26 Dec 2023 01:56:33 +0100 Subject: [PATCH 03/35] buy limit order example --- .../src/domain/competition/solution/trade.rs | 75 +++++++++++++++++-- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 9dc3ce2e61..a45ca552ac 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -69,13 +69,12 @@ impl Fulfillment { .ok_or(InvalidExecutedAmount)?; // Sold exactly `executed_sell_amount_with_surplus_fee` while the // limit price is - // `limit_sell_amount` Take protocol fee from the price - // improvement - let price_improvement_fee = limit_sell_amount + // `limit_sell_amount` Take protocol fee from the surplus + let surplus = limit_sell_amount .checked_sub(executed_sell_amount_with_surplus_fee) - .ok_or(InvalidExecutedAmount)? - * (eth::U256::from_f64_lossy(factor * 100.)) - / 100; + .ok_or(InvalidExecutedAmount)?; + let price_improvement_fee = + surplus * (eth::U256::from_f64_lossy(factor * 100.)) / 100; let max_volume_fee = executed_sell_amount_with_surplus_fee * (eth::U256::from_f64_lossy(max_volume_factor * 100.)) / 100; @@ -339,7 +338,7 @@ mod tests { }; #[test] - fn test_fulfillment() { + fn test_fulfillment_sell_limit_order_fok() { // https://explorer.cow.fi/orders/0xef6de27933bde867c768ead05d34a08c806d35b89f6bea565bdeb40108265e9a6f419390da10911abd1e1c962b569312a9c9c7b1658a2936?tab=overview let order = competition::Order { uid: Default::default(), @@ -395,11 +394,71 @@ mod tests { fulfillment.fee(), order::SellAmount((16799999999999998u128 + 306723471216604081u128).into()) ); - println!("{:?}", fulfillment.fee()); // executed amount reduced by protocol fee assert_eq!( fulfillment.executed(), U256::from(3843143195450062587u128).into() ); // 4149866666666666668 - 306723471216604081 } + + #[test] + pub fn test_fullfilment_buy_limit_order_fok() { + // https://explorer.cow.fi/orders/0xc9096a3dbfb1f661e65ecc14644adec6bd8e385ae818aa73181def24996affb589e4042fd85e857e81a4fa89831b1f5ad4f384b7659357d7?tab=overview + let order = competition::Order { + uid: Default::default(), + side: order::Side::Buy, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), + )), + amount: eth::TokenAmount(170000000000000000u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab").unwrap(), + )), + amount: eth::TokenAmount(1781433576205823004786u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::No, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=cYSDo4wBlutGF6Gybl6x + let uniform_sell_price = eth::U256::from(7213317128720734077u128); + let uniform_buy_price = eth::U256::from(74745150907421124481191u128); + let executed = order::TargetAmount(170000000000000000u128.into()); + let fee = Fee::Dynamic(order::SellAmount(19868323826701104280u128.into())); + let fulfillment = Fulfillment::new( + order.clone(), + executed, + fee, + uniform_sell_price, + uniform_buy_price, + ) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((19868323826701104280u128 + 3684441086061450u128).into()) + ); + // executed amount same as before + assert_eq!(fulfillment.executed(), executed); + } } From ffe84f9069d6d8defdb000d3b0a2899309b2321c Mon Sep 17 00:00:00 2001 From: sunce86 Date: Tue, 26 Dec 2023 02:10:34 +0100 Subject: [PATCH 04/35] pretty format --- .../driver/src/domain/competition/order/mod.rs | 16 ++++++++++++++++ .../src/domain/competition/solution/trade.rs | 13 +++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/driver/src/domain/competition/order/mod.rs b/crates/driver/src/domain/competition/order/mod.rs index 4135ab54bf..7c4ea1bed9 100644 --- a/crates/driver/src/domain/competition/order/mod.rs +++ b/crates/driver/src/domain/competition/order/mod.rs @@ -68,6 +68,14 @@ impl From for eth::U256 { } } +impl std::ops::Add for SellAmount { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + /// An amount denominated in the sell token for [`Side::Sell`] [`Order`]s, or in /// the buy token for [`Side::Buy`] [`Order`]s. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -97,6 +105,14 @@ impl From for eth::TokenAmount { } } +impl std::ops::Add for TargetAmount { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + /// Order fee denominated in the sell token. #[derive(Debug, Default, Clone, Copy)] pub struct Fee { diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index a45ca552ac..60c037c84b 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -128,7 +128,7 @@ impl Fulfillment { // Adjust the executed amount by the protocol fee. This is because solvers are // unaware of the protocol fee that driver introduces and they only account - // for the network fee. + // for the surplus fee. let executed = match order.side { order::Side::Buy => executed, order::Side::Sell => order::TargetAmount( @@ -158,12 +158,9 @@ impl Fulfillment { match order.partial { order::Partial::Yes { available } => { - order::TargetAmount(executed.0 + surplus_fee.0 + protocol_fee.0) <= available - } - order::Partial::No => { - order::TargetAmount(executed.0 + surplus_fee.0 + protocol_fee.0) - == order.target() + executed + surplus_fee + protocol_fee <= available } + order::Partial::No => executed + surplus_fee + protocol_fee == order.target(), } }; @@ -199,7 +196,7 @@ impl Fulfillment { pub fn scoring_fee(&self) -> order::SellAmount { match self.fee { Fee::Static => self.order.fee.solver, - Fee::Dynamic(fee) => (fee.0 + self.protocol_fee.0).into(), + Fee::Dynamic(fee) => fee + self.protocol_fee, } } @@ -208,7 +205,7 @@ impl Fulfillment { pub fn fee(&self) -> order::SellAmount { match self.fee { Fee::Static => self.order.fee.user, - Fee::Dynamic(fee) => (fee.0 + self.protocol_fee.0).into(), + Fee::Dynamic(fee) => fee + self.protocol_fee, } } From 950e7477d6d60af5ec1581c6984c14e6c5f75025 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Tue, 26 Dec 2023 02:30:11 +0100 Subject: [PATCH 05/35] fix comments --- .../src/domain/competition/solution/trade.rs | 77 +++++++++++-------- .../driver/src/infra/solver/dto/solution.rs | 4 +- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 60c037c84b..bfbeae418e 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -33,7 +33,7 @@ impl Fulfillment { fee: Fee, uniform_sell_price: eth::U256, uniform_buy_price: eth::U256, - ) -> Result { + ) -> Result { let protocol_fee = { let surplus_fee = match fee { Fee::Static => eth::U256::default(), @@ -49,30 +49,36 @@ impl Fulfillment { } => { let fee = match order.side { order::Side::Buy => { - // Equal to full sell amount for FOK orders, otherwise scalled with - // executed amount for partially - // fillable orders - let limit_sell_amount = - order.sell.amount.0 * executed.0 / order.buy.amount.0; // How much `sell_token` we need to sell to buy `executed` amount of // `buy_token` let executed_sell_amount = executed .0 .checked_mul(uniform_buy_price) - .ok_or(InvalidExecutedAmount)? + .ok_or(InvalidFullfilment)? .checked_div(uniform_sell_price) - .ok_or(InvalidExecutedAmount)?; + .ok_or(InvalidFullfilment)?; // We have to sell slightly more `sell_token` to capture the // `surplus_fee` let executed_sell_amount_with_surplus_fee = executed_sell_amount .checked_add(surplus_fee) - .ok_or(InvalidExecutedAmount)?; - // Sold exactly `executed_sell_amount_with_surplus_fee` while the - // limit price is - // `limit_sell_amount` Take protocol fee from the surplus + .ok_or(InvalidFullfilment)?; + // What is the maximum amount of `sell_token` we are allowed to + // sell based on limit price? + // Equal to full sell amount for FOK orders, otherwise scalled with + // executed amount for partially fillable orders + let limit_sell_amount = order + .sell + .amount + .0 + .checked_mul(executed.0) + .ok_or(InvalidFullfilment)? + .checked_div(order.buy.amount.0) + .ok_or(InvalidFullfilment)?; + // Take protocol fee from the surplus + // Surplus is the diff between the limit price and executed amount let surplus = limit_sell_amount .checked_sub(executed_sell_amount_with_surplus_fee) - .ok_or(InvalidExecutedAmount)?; + .ok_or(InvalidFullfilment)?; let price_improvement_fee = surplus * (eth::U256::from_f64_lossy(factor * 100.)) / 100; let max_volume_fee = executed_sell_amount_with_surplus_fee @@ -82,32 +88,39 @@ impl Fulfillment { std::cmp::min(price_improvement_fee, max_volume_fee) } order::Side::Sell => { - let executed_sell_amount = executed - .0 - .checked_add(surplus_fee) - .ok_or(InvalidExecutedAmount)?; - // Equal to full buy amount for FOK orders, otherwise scalled with - // executed amount for partially - // fillable orders - let limit_buy_amount = - order.buy.amount.0 * executed_sell_amount / order.sell.amount.0; // How much `buy_token` we get for `executed` amount of `sell_token` let executed_buy_amount = executed .0 .checked_mul(uniform_sell_price) - .ok_or(InvalidExecutedAmount)? + .ok_or(InvalidFullfilment)? .checked_div(uniform_buy_price) - .ok_or(InvalidExecutedAmount)?; + .ok_or(InvalidFullfilment)?; + let executed_sell_amount = executed + .0 + .checked_add(surplus_fee) + .ok_or(InvalidFullfilment)?; + // What is the minimum amount of `buy_token` we have to buy based on + // limit price? + // Equal to full buy amount for FOK orders, otherwise scalled with + // executed amount for partially fillable orders + let limit_buy_amount = order + .buy + .amount + .0 + .checked_mul(executed_sell_amount) + .ok_or(InvalidFullfilment)? + .checked_div(order.sell.amount.0) + .ok_or(InvalidFullfilment)?; // Bought exactly `executed_buy_amount` while the limit price is // `limit_buy_amount` Take protocol fee from the surplus let surplus = executed_buy_amount .checked_sub(limit_buy_amount) - .ok_or(InvalidExecutedAmount)?; + .ok_or(InvalidFullfilment)?; let surplus_in_sell_token = surplus .checked_mul(uniform_buy_price) - .ok_or(InvalidExecutedAmount)? + .ok_or(InvalidFullfilment)? .checked_div(uniform_sell_price) - .ok_or(InvalidExecutedAmount)?; + .ok_or(InvalidFullfilment)?; let price_improvement_fee = surplus_in_sell_token * (eth::U256::from_f64_lossy(factor * 100.)) / 100; @@ -135,7 +148,7 @@ impl Fulfillment { executed .0 .checked_sub(protocol_fee.0) - .ok_or(InvalidExecutedAmount)?, + .ok_or(InvalidFullfilment)?, ), }; @@ -179,7 +192,7 @@ impl Fulfillment { protocol_fee, }) } else { - Err(InvalidExecutedAmount) + Err(InvalidFullfilment) } } @@ -270,7 +283,7 @@ impl Jit { pub fn new( order: order::Jit, executed: order::TargetAmount, - ) -> Result { + ) -> Result { // If the order is partially fillable, the executed amount can be smaller than // the target amount. Otherwise, the executed amount must be equal to the target // amount. @@ -282,7 +295,7 @@ impl Jit { if is_valid { Ok(Self { order, executed }) } else { - Err(InvalidExecutedAmount) + Err(InvalidFullfilment) } } @@ -306,7 +319,7 @@ pub struct Execution { #[derive(Debug, thiserror::Error)] #[error("invalid executed amount")] -pub struct InvalidExecutedAmount; +pub struct InvalidFullfilment; #[derive(Debug, thiserror::Error)] pub enum ExecutionError { diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index e1ec782bd5..eb962ffd4c 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -55,7 +55,7 @@ impl Solutions { ) .map(competition::solution::Trade::Fulfillment) .map_err( - |competition::solution::trade::InvalidExecutedAmount| { + |competition::solution::trade::InvalidFullfilment| { super::Error("invalid trade fulfillment") }, ) @@ -121,7 +121,7 @@ impl Solutions { jit.executed_amount.into(), ) .map_err( - |competition::solution::trade::InvalidExecutedAmount| { + |competition::solution::trade::InvalidFullfilment| { super::Error("invalid executed amount in JIT order") }, )?, From 6db55cd6c6a417360b16519343794c20d225b648 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Tue, 26 Dec 2023 23:32:58 +0100 Subject: [PATCH 06/35] added unit test for partial limit order --- .../src/domain/competition/solution/trade.rs | 194 +++++++++++++++++- 1 file changed, 193 insertions(+), 1 deletion(-) diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index bfbeae418e..6366a09e7a 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -340,6 +340,7 @@ mod tests { FeePolicy, SellTokenBalance, Signature, + TargetAmount, }, util, }, @@ -412,7 +413,7 @@ mod tests { } #[test] - pub fn test_fullfilment_buy_limit_order_fok() { + pub fn test_fulfillment_buy_limit_order_fok() { // https://explorer.cow.fi/orders/0xc9096a3dbfb1f661e65ecc14644adec6bd8e385ae818aa73181def24996affb589e4042fd85e857e81a4fa89831b1f5ad4f384b7659357d7?tab=overview let order = competition::Order { uid: Default::default(), @@ -471,4 +472,195 @@ mod tests { // executed amount same as before assert_eq!(fulfillment.executed(), executed); } + + #[test] + fn test_fulfillment_sell_limit_order_partial() { + // https://explorer.cow.fi/orders/0x1a146dba48512326c647aae1ce511206b373b151e1b9ada9772c313e7d24ec2e0960da039bb8151cacfef620476e8baf34bd95656594209e?tab=overview + // 3 fullfillments + // + // 1. tx hash 0xbc95b97d09a62e6a68b15a8dfd4655a6e25d100ce0dd98a6a43e3b7eac9951cc + // + // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=W-uxp4wBlutGF6GyxkCq + let order1 = competition::Order { + uid: Default::default(), + side: order::Side::Sell, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), + )), + amount: eth::TokenAmount(136363636363636u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + amount: eth::TokenAmount(9000000000u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::Yes { + available: TargetAmount(9000000000u128.into()), + }, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + let uniform_sell_price = eth::U256::from(452471455796126723289489746u128); + let uniform_buy_price = eth::U256::from(29563373796548615411833u128); + let executed = order::TargetAmount(1746031488u128.into()); + let fee = Fee::Dynamic(order::SellAmount(11566733u128.into())); + let fulfillment = Fulfillment::new( + order1.clone(), + executed, + fee, + uniform_sell_price, + uniform_buy_price, + ) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((11566733u128 + 3037322u128).into()) + ); + // executed amount reduced by protocol fee + assert_eq!(fulfillment.executed(), U256::from(1742994166u128).into()); // 1746031488 - 3037322 + + // 2. tx hash 0x2f9b928182649aad2eaf04361fff1aff3cb8d37e4988c952aed49465eff01c9e + // + // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=uvXcp4wB4Ql8nk7aQgeZ + + let order2 = competition::Order { + uid: Default::default(), + side: order::Side::Sell, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), + )), + amount: eth::TokenAmount(136363636363636u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + amount: eth::TokenAmount(9000000000u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::Yes { + available: TargetAmount(7242401779u128.into()), + }, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + let uniform_sell_price = eth::U256::from(49331008874302634851980418220032u128); + let uniform_buy_price = eth::U256::from(3204738565525085525012119552u128); + let executed = order::TargetAmount(2887238741u128.into()); + let fee = Fee::Dynamic(order::SellAmount(27827963u128.into())); + let fulfillment = Fulfillment::new( + order2.clone(), + executed, + fee, + uniform_sell_price, + uniform_buy_price, + ) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((27827963u128 + 8965365u128).into()) + ); + // executed amount reduced by protocol fee + assert_eq!(fulfillment.executed(), U256::from(2878273376u128).into()); // 2887238741 - 8965365 + + // 3. 0x813dab5983fd3643e1ce3e7efbdbfe1ca8c41419bcfaf1e898e067e37c455d75 + // + // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=xPXdp4wB4Ql8nk7a8ert + + let order3 = competition::Order { + uid: Default::default(), + side: order::Side::Sell, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), + )), + amount: eth::TokenAmount(136363636363636u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + amount: eth::TokenAmount(9000000000u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::Yes { + available: TargetAmount(4327335075u128.into()), + }, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + let uniform_sell_price = eth::U256::from(65841033847428u128); + let uniform_buy_price = eth::U256::from(4302554937u128); + let executed = order::TargetAmount(4302554937u128.into()); + let fee = Fee::Dynamic(order::SellAmount(24780138u128.into())); + let fulfillment = Fulfillment::new( + order3.clone(), + executed, + fee, + uniform_sell_price, + uniform_buy_price, + ) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((24780138u128 + 8996762u128).into()) + ); + // executed amount reduced by protocol fee + assert_eq!(fulfillment.executed(), U256::from(4293558175u128).into()); // 4302554937 - 8996762 + } } From 1de045638da38a5247bf8d98f4d99542b9a694c3 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 00:07:27 +0100 Subject: [PATCH 07/35] wrap eth --- crates/driver/src/infra/solver/dto/solution.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index eb962ffd4c..9f505ad6ea 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -38,8 +38,10 @@ impl Solutions { ))? .clone(); - let uniform_sell_price = solution.prices[&order.sell.token.into()]; - let uniform_buy_price = solution.prices[&order.buy.token.into()]; + let uniform_sell_price = + solution.prices[&order.sell.token.wrap(weth).into()]; + let uniform_buy_price = + solution.prices[&order.buy.token.wrap(weth).into()]; competition::solution::trade::Fulfillment::new( order, From 9d5abd6dcde2230711c07458ef1647892cadcfb3 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 00:16:04 +0100 Subject: [PATCH 08/35] volume based fee --- .../src/domain/competition/solution/trade.rs | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 6366a09e7a..bb2b7927c6 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -42,12 +42,12 @@ impl Fulfillment { let mut protocol_fee = Default::default(); for fee_policy in &order.fee_policies { - match fee_policy { + let fee = match fee_policy { order::FeePolicy::PriceImprovement { factor, max_volume_factor, } => { - let fee = match order.side { + match order.side { order::Side::Buy => { // How much `sell_token` we need to sell to buy `executed` amount of // `buy_token` @@ -130,11 +130,40 @@ impl Fulfillment { // take the smaller of the two std::cmp::min(price_improvement_fee, max_volume_fee) } - }; - protocol_fee += fee; + } } - order::FeePolicy::Volume { factor: _ } => unimplemented!(), - } + order::FeePolicy::Volume { factor } => { + match order.side { + order::Side::Buy => { + // How much `sell_token` we need to sell to buy `executed` amount of + // `buy_token` + let executed_sell_amount = executed + .0 + .checked_mul(uniform_buy_price) + .ok_or(InvalidFullfilment)? + .checked_div(uniform_sell_price) + .ok_or(InvalidFullfilment)?; + // We have to sell slightly more `sell_token` to capture the + // `surplus_fee` + let executed_sell_amount_with_surplus_fee = executed_sell_amount + .checked_add(surplus_fee) + .ok_or(InvalidFullfilment)?; + executed_sell_amount_with_surplus_fee + * (eth::U256::from_f64_lossy(factor * 100.)) + / 100 + } + order::Side::Sell => { + let executed_sell_amount = executed + .0 + .checked_add(surplus_fee) + .ok_or(InvalidFullfilment)?; + executed_sell_amount * (eth::U256::from_f64_lossy(factor * 100.)) + / 100 + } + } + } + }; + protocol_fee += fee; } order::SellAmount(protocol_fee) }; From 06da3eff7ad8d80b699535a7b6ffad94aca547b6 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 00:23:41 +0100 Subject: [PATCH 09/35] zero surplus is ok --- .../driver/src/domain/competition/solution/trade.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index bb2b7927c6..bb8ec68956 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -78,7 +78,7 @@ impl Fulfillment { // Surplus is the diff between the limit price and executed amount let surplus = limit_sell_amount .checked_sub(executed_sell_amount_with_surplus_fee) - .ok_or(InvalidFullfilment)?; + .unwrap_or(eth::U256::zero()); let price_improvement_fee = surplus * (eth::U256::from_f64_lossy(factor * 100.)) / 100; let max_volume_fee = executed_sell_amount_with_surplus_fee @@ -115,7 +115,7 @@ impl Fulfillment { // `limit_buy_amount` Take protocol fee from the surplus let surplus = executed_buy_amount .checked_sub(limit_buy_amount) - .ok_or(InvalidFullfilment)?; + .unwrap_or(eth::U256::zero()); let surplus_in_sell_token = surplus .checked_mul(uniform_buy_price) .ok_or(InvalidFullfilment)? @@ -692,4 +692,13 @@ mod tests { // executed amount reduced by protocol fee assert_eq!(fulfillment.executed(), U256::from(4293558175u128).into()); // 4302554937 - 8996762 } + + #[test] + fn test_checked_sub() { + assert_eq!(U256::from(1u128).checked_sub(U256::from(2u128)), None); + assert_eq!( + U256::from(2u128).checked_sub(U256::from(1u128)), + Some(U256::from(1u128)) + ); + } } From 1bff25131cab53de2927b7428399f6f312fc8ebb Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 00:29:17 +0100 Subject: [PATCH 10/35] add protocol fee to static also --- crates/driver/src/domain/competition/solution/trade.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index bb8ec68956..f8a6cea485 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -237,7 +237,7 @@ impl Fulfillment { /// scoring a solution. pub fn scoring_fee(&self) -> order::SellAmount { match self.fee { - Fee::Static => self.order.fee.solver, + Fee::Static => self.order.fee.solver + self.protocol_fee, Fee::Dynamic(fee) => fee + self.protocol_fee, } } @@ -246,7 +246,7 @@ impl Fulfillment { /// considering their signed order and the uniform clearing prices pub fn fee(&self) -> order::SellAmount { match self.fee { - Fee::Static => self.order.fee.user, + Fee::Static => self.order.fee.user + self.protocol_fee, Fee::Dynamic(fee) => fee + self.protocol_fee, } } From 04c1c5ab3fa397e173c06bee58215f9e27bfe831 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 00:36:22 +0100 Subject: [PATCH 11/35] fee policies for limit orders only --- crates/driver/src/tests/setup/driver.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 25843d01c0..9cc4bc1994 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -100,15 +100,19 @@ pub fn solve_req(test: &Test) -> serde_json::Value { "appData": "0x0000000000000000000000000000000000000000000000000000000000000000", "signingScheme": "eip712", "signature": format!("0x{}", hex::encode(quote.order_signature(&test.blockchain))), - "feePolicies": [{ - "kind": "priceimprovement", - "factor": 0.5, - "maxVolumeFactor": 0.06, + "feePolicies": match quote.order.kind { + order::Kind::Market => [], + order::Kind::Liquidity => [], + order::Kind::Limit { .. } => [{ + "kind": "priceimprovement", + "factor": 0.5, + "maxVolumeFactor": 0.06, + }, + { + "kind": "volume", + "factor": 0.1, + }], }, - { - "kind": "volume", - "factor": 0.1, - }], })); } for fulfillment in test.fulfillments.iter() { From 42b442da0615bfaba474d4b65a5cc9a0848868c5 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 00:37:24 +0100 Subject: [PATCH 12/35] revert static --- crates/driver/src/domain/competition/solution/trade.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index f8a6cea485..bb8ec68956 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -237,7 +237,7 @@ impl Fulfillment { /// scoring a solution. pub fn scoring_fee(&self) -> order::SellAmount { match self.fee { - Fee::Static => self.order.fee.solver + self.protocol_fee, + Fee::Static => self.order.fee.solver, Fee::Dynamic(fee) => fee + self.protocol_fee, } } @@ -246,7 +246,7 @@ impl Fulfillment { /// considering their signed order and the uniform clearing prices pub fn fee(&self) -> order::SellAmount { match self.fee { - Fee::Static => self.order.fee.user + self.protocol_fee, + Fee::Static => self.order.fee.user, Fee::Dynamic(fee) => fee + self.protocol_fee, } } From bfd38a1375a4f5d14ca18ff00a7b0150e63af9e4 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 00:46:43 +0100 Subject: [PATCH 13/35] fix previous --- crates/driver/src/tests/setup/driver.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 9cc4bc1994..6f225d87cd 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -101,17 +101,19 @@ pub fn solve_req(test: &Test) -> serde_json::Value { "signingScheme": "eip712", "signature": format!("0x{}", hex::encode(quote.order_signature(&test.blockchain))), "feePolicies": match quote.order.kind { - order::Kind::Market => [], - order::Kind::Liquidity => [], - order::Kind::Limit { .. } => [{ - "kind": "priceimprovement", - "factor": 0.5, - "maxVolumeFactor": 0.06, - }, - { - "kind": "volume", - "factor": 0.1, - }], + order::Kind::Market => vec![], + order::Kind::Liquidity => vec![], + order::Kind::Limit { .. } => vec![ + json!({ + "kind": "priceimprovement", + "factor": 0.5, + "maxVolumeFactor": 0.06, + }), + json!({ + "kind": "volume", + "factor": 0.1, + }) + ], }, })); } From 3176004cc0f8111c6f79bf89be433bbc26cbf4c2 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 15:27:26 +0100 Subject: [PATCH 14/35] refactor --- .../src/domain/competition/solution/fee.rs | 562 ++++++++++++++++++ .../src/domain/competition/solution/mod.rs | 1 + .../src/domain/competition/solution/trade.rs | 518 +--------------- .../driver/src/infra/solver/dto/solution.rs | 9 +- 4 files changed, 574 insertions(+), 516 deletions(-) create mode 100644 crates/driver/src/domain/competition/solution/fee.rs diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs new file mode 100644 index 0000000000..970f3807ed --- /dev/null +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -0,0 +1,562 @@ +//! Applies the protocol fee to the solution received from the solver. +//! +//! Protocol fee is applied by increasing already existing fee determined by the +//! solver. + +use { + super::trade::{Fee, Fulfillment, InvalidFullfilment}, + crate::domain::{ + competition::{ + order, + order::{FeePolicy, Side}, + }, + eth, + }, +}; + +impl Fulfillment { + pub fn apply_protocol_fee( + self, + uniform_sell_price: eth::U256, + uniform_buy_price: eth::U256, + ) -> Result { + let order = self.order().clone(); + let executed = self.executed(); + let surplus_fee = match self.raw_fee() { + Fee::Static => eth::U256::default(), + Fee::Dynamic(fee) => fee.0, + }; + + let protocol_fee = order + .fee_policies + .iter() + .map(|fee_policy| { + match (fee_policy, order.side) { + ( + FeePolicy::PriceImprovement { + factor, + max_volume_factor, + }, + Side::Buy, + ) => { + let price_improvement_fee = price_improvement_fee_for_buy( + order.sell.amount.0, + order.buy.amount.0, + executed.0, + surplus_fee, + uniform_sell_price, + uniform_buy_price, + *factor, + )?; + let max_volume_fee = volume_fee_for_buy( + executed.0, + surplus_fee, + uniform_sell_price, + uniform_buy_price, + *max_volume_factor, + )?; + // take the smaller of the two + Some(std::cmp::min(price_improvement_fee, max_volume_fee)) + } + ( + FeePolicy::PriceImprovement { + factor, + max_volume_factor, + }, + Side::Sell, + ) => { + let price_improvement_fee = price_improvement_fee_for_sell( + order.sell.amount.0, + order.buy.amount.0, + executed.0, + surplus_fee, + uniform_sell_price, + uniform_buy_price, + *factor, + )?; + let max_volume_fee = + volume_fee_for_sell(executed.0, surplus_fee, *max_volume_factor)?; + // take the smaller of the two + Some(std::cmp::min(price_improvement_fee, max_volume_fee)) + } + (FeePolicy::Volume { factor }, Side::Buy) => volume_fee_for_buy( + executed.0, + surplus_fee, + uniform_sell_price, + uniform_buy_price, + *factor, + ), + (FeePolicy::Volume { factor }, Side::Sell) => { + volume_fee_for_sell(executed.0, surplus_fee, *factor) + } + } + }) + .fold(eth::U256::zero(), |acc, fee| match fee { + Some(fee) => acc + fee, + None => { + tracing::warn!(?order.uid, "failed to calculate protocol fee"); + acc + } + }) + .into(); + + // Increase the fee by the protocol fee + let fee = match self.raw_fee() { + Fee::Static => Fee::Static, + Fee::Dynamic(fee) => Fee::Dynamic(fee + protocol_fee), + }; + + // Adjust the executed amount by the protocol fee. This is because solvers are + // unaware of the protocol fee that driver introduces and they only account + // for the surplus fee. + let executed = match order.side { + order::Side::Buy => executed, + order::Side::Sell => order::TargetAmount( + executed + .0 + .checked_sub(protocol_fee.0) + .ok_or(InvalidFullfilment)?, + ), + }; + + Fulfillment::new(order, executed, fee) + } +} + +fn price_improvement_fee_for_buy( + sell_amount: eth::U256, + buy_amount: eth::U256, + executed_buy_amount: eth::U256, + surplus_fee: eth::U256, + uniform_sell_price: eth::U256, + uniform_buy_price: eth::U256, + factor: f64, +) -> Option { + // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` + let executed_sell_amount = executed_buy_amount + .checked_mul(uniform_buy_price)? + .checked_div(uniform_sell_price)?; + // Sell slightly more `sell_token` to capture the `surplus_fee` + let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; + // Scale to support partially fillable orders + let limit_sell_amount = sell_amount + .checked_mul(executed_buy_amount)? + .checked_div(buy_amount)?; + // Remaining surplus after fees + let surplus = limit_sell_amount + .checked_sub(executed_sell_amount_with_fee) + .unwrap_or(eth::U256::zero()); + Some(surplus.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) +} + +fn price_improvement_fee_for_sell( + sell_amount: eth::U256, + buy_amount: eth::U256, + executed_sell_amount: eth::U256, + surplus_fee: eth::U256, + uniform_sell_price: eth::U256, + uniform_buy_price: eth::U256, + factor: f64, +) -> Option { + // How much `buy_token` we get for `executed` amount of `sell_token` + let executed_buy_amount = executed_sell_amount + .checked_mul(uniform_sell_price)? + .checked_div(uniform_buy_price)?; + let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; + // Scale to support partially fillable orders + let limit_buy_amount = buy_amount + .checked_mul(executed_sell_amount_with_fee)? + .checked_div(sell_amount)?; + // Remaining surplus after fees + let surplus = executed_buy_amount + .checked_sub(limit_buy_amount) + .unwrap_or(eth::U256::zero()); + let surplus_in_sell_token = surplus + .checked_mul(uniform_buy_price)? + .checked_div(uniform_sell_price)?; + Some(surplus_in_sell_token.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) +} + +fn volume_fee_for_buy( + executed_buy_amount: eth::U256, + surplus_fee: eth::U256, + uniform_sell_price: eth::U256, + uniform_buy_price: eth::U256, + factor: f64, +) -> Option { + // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` + let executed_sell_amount = executed_buy_amount + .checked_mul(uniform_buy_price)? + .checked_div(uniform_sell_price)?; + // Sell slightly more `sell_token` to capture the `surplus_fee` + let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; + Some(executed_sell_amount_with_fee.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) +} + +fn volume_fee_for_sell( + executed_sell_amount: eth::U256, + surplus_fee: eth::U256, + factor: f64, +) -> Option { + let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; + Some(executed_sell_amount_with_fee.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) +} + +mod tests { + use { + super::*, + crate::{ + domain::{ + competition, + competition::order::{ + signature::Scheme, + AppData, + BuyTokenBalance, + FeePolicy, + SellTokenBalance, + Signature, + TargetAmount, + }, + }, + util, + }, + primitive_types::{H160, U256}, + std::str::FromStr, + }; + + #[test] + fn test_fulfillment_sell_limit_order_fok() { + // https://explorer.cow.fi/orders/0xef6de27933bde867c768ead05d34a08c806d35b89f6bea565bdeb40108265e9a6f419390da10911abd1e1c962b569312a9c9c7b1658a2936?tab=overview + let order = competition::Order { + uid: Default::default(), + side: order::Side::Sell, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xba3335588d9403515223f109edc4eb7269a9ab5d").unwrap(), + )), + amount: eth::TokenAmount(778310860032541096349039u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), + )), + amount: eth::TokenAmount(4166666666666666666u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::No, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.25?id=m8dnoowB4Ql8nk7a5ber + let uniform_sell_price = eth::U256::from(913320970421237626580182u128); + let uniform_buy_price = eth::U256::from(4149866666666666668u128); + let executed = order::TargetAmount(4149866666666666668u128.into()); + let fee = Fee::Dynamic(order::SellAmount(16799999999999998u128.into())); + let fulfillment = Fulfillment::new(order.clone(), executed, fee).unwrap(); + // fee does not contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount(16799999999999998u128.into()) + ); + // executed amount before protocol fee + assert_eq!(fulfillment.executed(), executed); + + let fulfillment = fulfillment + .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((16799999999999998u128 + 306723471216604081u128).into()) + ); + // executed amount reduced by protocol fee + assert_eq!( + fulfillment.executed(), + U256::from(3843143195450062587u128).into() + ); // 4149866666666666668 - 306723471216604081 + } + + #[test] + pub fn test_fulfillment_buy_limit_order_fok() { + // https://explorer.cow.fi/orders/0xc9096a3dbfb1f661e65ecc14644adec6bd8e385ae818aa73181def24996affb589e4042fd85e857e81a4fa89831b1f5ad4f384b7659357d7?tab=overview + let order = competition::Order { + uid: Default::default(), + side: order::Side::Buy, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), + )), + amount: eth::TokenAmount(170000000000000000u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab").unwrap(), + )), + amount: eth::TokenAmount(1781433576205823004786u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::No, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=cYSDo4wBlutGF6Gybl6x + let uniform_sell_price = eth::U256::from(7213317128720734077u128); + let uniform_buy_price = eth::U256::from(74745150907421124481191u128); + let executed = order::TargetAmount(170000000000000000u128.into()); + let fee = Fee::Dynamic(order::SellAmount(19868323826701104280u128.into())); + let fulfillment = Fulfillment::new(order.clone(), executed, fee).unwrap(); + // fee does not contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount(19868323826701104280u128.into()) + ); + // executed amount before protocol fee + assert_eq!(fulfillment.executed(), executed); + + let fulfillment = fulfillment + .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((19868323826701104280u128 + 3684441086061450u128).into()) + ); + // executed amount same as before + assert_eq!(fulfillment.executed(), executed); + } + + #[test] + fn test_fulfillment_sell_limit_order_partial() { + // https://explorer.cow.fi/orders/0x1a146dba48512326c647aae1ce511206b373b151e1b9ada9772c313e7d24ec2e0960da039bb8151cacfef620476e8baf34bd95656594209e?tab=overview + // 3 fullfillments + // + // 1. tx hash 0xbc95b97d09a62e6a68b15a8dfd4655a6e25d100ce0dd98a6a43e3b7eac9951cc + // + // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=W-uxp4wBlutGF6GyxkCq + let order1 = competition::Order { + uid: Default::default(), + side: order::Side::Sell, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), + )), + amount: eth::TokenAmount(136363636363636u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + amount: eth::TokenAmount(9000000000u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::Yes { + available: TargetAmount(9000000000u128.into()), + }, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + let uniform_sell_price = eth::U256::from(452471455796126723289489746u128); + let uniform_buy_price = eth::U256::from(29563373796548615411833u128); + let executed = order::TargetAmount(1746031488u128.into()); + let fee = Fee::Dynamic(order::SellAmount(11566733u128.into())); + let fulfillment = Fulfillment::new(order1.clone(), executed, fee).unwrap(); + // fee does not contains protocol fee + assert_eq!(fulfillment.fee(), order::SellAmount(11566733u128.into())); + // executed amount before protocol fee + assert_eq!(fulfillment.executed(), executed); + + let fulfillment = fulfillment + .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((11566733u128 + 3037322u128).into()) + ); + // executed amount reduced by protocol fee + assert_eq!(fulfillment.executed(), U256::from(1742994166u128).into()); // 1746031488 - 3037322 + + // 2. tx hash 0x2f9b928182649aad2eaf04361fff1aff3cb8d37e4988c952aed49465eff01c9e + // + // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=uvXcp4wB4Ql8nk7aQgeZ + + let order2 = competition::Order { + uid: Default::default(), + side: order::Side::Sell, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), + )), + amount: eth::TokenAmount(136363636363636u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + amount: eth::TokenAmount(9000000000u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::Yes { + available: TargetAmount(7242401779u128.into()), + }, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + let uniform_sell_price = eth::U256::from(49331008874302634851980418220032u128); + let uniform_buy_price = eth::U256::from(3204738565525085525012119552u128); + let executed = order::TargetAmount(2887238741u128.into()); + let fee = Fee::Dynamic(order::SellAmount(27827963u128.into())); + let fulfillment = Fulfillment::new(order2.clone(), executed, fee).unwrap(); + // fee does not contains protocol fee + assert_eq!(fulfillment.fee(), order::SellAmount(27827963u128.into())); + // executed amount before protocol fee + assert_eq!(fulfillment.executed(), executed); + + let fulfillment = fulfillment + .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((27827963u128 + 8965365u128).into()) + ); + // executed amount reduced by protocol fee + assert_eq!(fulfillment.executed(), U256::from(2878273376u128).into()); // 2887238741 - 8965365 + + // 3. 0x813dab5983fd3643e1ce3e7efbdbfe1ca8c41419bcfaf1e898e067e37c455d75 + // + // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=xPXdp4wB4Ql8nk7a8ert + + let order3 = competition::Order { + uid: Default::default(), + side: order::Side::Sell, + buy: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), + )), + amount: eth::TokenAmount(136363636363636u128.into()), + }, + sell: eth::Asset { + token: eth::TokenAddress(eth::ContractAddress( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + amount: eth::TokenAmount(9000000000u128.into()), + }, + kind: order::Kind::Limit, + fee: Default::default(), + fee_policies: vec![FeePolicy::PriceImprovement { + factor: 0.5, + max_volume_factor: 1.0, + }], + partial: order::Partial::Yes { + available: TargetAmount(4327335075u128.into()), + }, + receiver: Default::default(), + pre_interactions: Default::default(), + post_interactions: Default::default(), + valid_to: util::Timestamp(0), + app_data: AppData(Default::default()), + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: Scheme::Eip712, + data: Default::default(), + signer: eth::Address::default(), + }, + }; + + let uniform_sell_price = eth::U256::from(65841033847428u128); + let uniform_buy_price = eth::U256::from(4302554937u128); + let executed = order::TargetAmount(4302554937u128.into()); + let fee = Fee::Dynamic(order::SellAmount(24780138u128.into())); + let fulfillment = Fulfillment::new(order3.clone(), executed, fee).unwrap(); + // fee does not contains protocol fee + assert_eq!(fulfillment.fee(), order::SellAmount(24780138u128.into())); + // executed amount before protocol fee + assert_eq!(fulfillment.executed(), executed); + + let fulfillment = fulfillment + .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .unwrap(); + // fee contains protocol fee + assert_eq!( + fulfillment.fee(), + order::SellAmount((24780138u128 + 8996762u128).into()) + ); + // executed amount reduced by protocol fee + assert_eq!(fulfillment.executed(), U256::from(4293558175u128).into()); // 4302554937 - 8996762 + } + + #[test] + fn test_checked_sub() { + assert_eq!(U256::from(1u128).checked_sub(U256::from(2u128)), None); + assert_eq!( + U256::from(2u128).checked_sub(U256::from(1u128)), + Some(U256::from(1u128)) + ); + } +} diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 6ab6773aa0..b1820b8eaa 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -19,6 +19,7 @@ use { thiserror::Error, }; +pub mod fee; pub mod interaction; pub mod settlement; pub mod trade; diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index bb8ec68956..27306618de 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -23,7 +23,6 @@ pub struct Fulfillment { /// order. executed: order::TargetAmount, fee: Fee, - protocol_fee: order::SellAmount, } impl Fulfillment { @@ -31,161 +30,12 @@ impl Fulfillment { order: competition::Order, executed: order::TargetAmount, fee: Fee, - uniform_sell_price: eth::U256, - uniform_buy_price: eth::U256, ) -> Result { - let protocol_fee = { - let surplus_fee = match fee { - Fee::Static => eth::U256::default(), - Fee::Dynamic(fee) => fee.0, - }; - - let mut protocol_fee = Default::default(); - for fee_policy in &order.fee_policies { - let fee = match fee_policy { - order::FeePolicy::PriceImprovement { - factor, - max_volume_factor, - } => { - match order.side { - order::Side::Buy => { - // How much `sell_token` we need to sell to buy `executed` amount of - // `buy_token` - let executed_sell_amount = executed - .0 - .checked_mul(uniform_buy_price) - .ok_or(InvalidFullfilment)? - .checked_div(uniform_sell_price) - .ok_or(InvalidFullfilment)?; - // We have to sell slightly more `sell_token` to capture the - // `surplus_fee` - let executed_sell_amount_with_surplus_fee = executed_sell_amount - .checked_add(surplus_fee) - .ok_or(InvalidFullfilment)?; - // What is the maximum amount of `sell_token` we are allowed to - // sell based on limit price? - // Equal to full sell amount for FOK orders, otherwise scalled with - // executed amount for partially fillable orders - let limit_sell_amount = order - .sell - .amount - .0 - .checked_mul(executed.0) - .ok_or(InvalidFullfilment)? - .checked_div(order.buy.amount.0) - .ok_or(InvalidFullfilment)?; - // Take protocol fee from the surplus - // Surplus is the diff between the limit price and executed amount - let surplus = limit_sell_amount - .checked_sub(executed_sell_amount_with_surplus_fee) - .unwrap_or(eth::U256::zero()); - let price_improvement_fee = - surplus * (eth::U256::from_f64_lossy(factor * 100.)) / 100; - let max_volume_fee = executed_sell_amount_with_surplus_fee - * (eth::U256::from_f64_lossy(max_volume_factor * 100.)) - / 100; - // take the smaller of the two - std::cmp::min(price_improvement_fee, max_volume_fee) - } - order::Side::Sell => { - // How much `buy_token` we get for `executed` amount of `sell_token` - let executed_buy_amount = executed - .0 - .checked_mul(uniform_sell_price) - .ok_or(InvalidFullfilment)? - .checked_div(uniform_buy_price) - .ok_or(InvalidFullfilment)?; - let executed_sell_amount = executed - .0 - .checked_add(surplus_fee) - .ok_or(InvalidFullfilment)?; - // What is the minimum amount of `buy_token` we have to buy based on - // limit price? - // Equal to full buy amount for FOK orders, otherwise scalled with - // executed amount for partially fillable orders - let limit_buy_amount = order - .buy - .amount - .0 - .checked_mul(executed_sell_amount) - .ok_or(InvalidFullfilment)? - .checked_div(order.sell.amount.0) - .ok_or(InvalidFullfilment)?; - // Bought exactly `executed_buy_amount` while the limit price is - // `limit_buy_amount` Take protocol fee from the surplus - let surplus = executed_buy_amount - .checked_sub(limit_buy_amount) - .unwrap_or(eth::U256::zero()); - let surplus_in_sell_token = surplus - .checked_mul(uniform_buy_price) - .ok_or(InvalidFullfilment)? - .checked_div(uniform_sell_price) - .ok_or(InvalidFullfilment)?; - let price_improvement_fee = surplus_in_sell_token - * (eth::U256::from_f64_lossy(factor * 100.)) - / 100; - let max_volume_fee = executed_sell_amount - * (eth::U256::from_f64_lossy(max_volume_factor * 100.)) - / 100; - // take the smaller of the two - std::cmp::min(price_improvement_fee, max_volume_fee) - } - } - } - order::FeePolicy::Volume { factor } => { - match order.side { - order::Side::Buy => { - // How much `sell_token` we need to sell to buy `executed` amount of - // `buy_token` - let executed_sell_amount = executed - .0 - .checked_mul(uniform_buy_price) - .ok_or(InvalidFullfilment)? - .checked_div(uniform_sell_price) - .ok_or(InvalidFullfilment)?; - // We have to sell slightly more `sell_token` to capture the - // `surplus_fee` - let executed_sell_amount_with_surplus_fee = executed_sell_amount - .checked_add(surplus_fee) - .ok_or(InvalidFullfilment)?; - executed_sell_amount_with_surplus_fee - * (eth::U256::from_f64_lossy(factor * 100.)) - / 100 - } - order::Side::Sell => { - let executed_sell_amount = executed - .0 - .checked_add(surplus_fee) - .ok_or(InvalidFullfilment)?; - executed_sell_amount * (eth::U256::from_f64_lossy(factor * 100.)) - / 100 - } - } - } - }; - protocol_fee += fee; - } - order::SellAmount(protocol_fee) - }; - - // Adjust the executed amount by the protocol fee. This is because solvers are - // unaware of the protocol fee that driver introduces and they only account - // for the surplus fee. - let executed = match order.side { - order::Side::Buy => executed, - order::Side::Sell => order::TargetAmount( - executed - .0 - .checked_sub(protocol_fee.0) - .ok_or(InvalidFullfilment)?, - ), - }; - // If the order is partial, the total executed amount can be smaller than // the target amount. Otherwise, the executed amount must be equal to the target // amount. let valid_execution = { - let surplus_fee = match order.side { + let fee = match order.side { order::Side::Buy => order::TargetAmount::default(), order::Side::Sell => order::TargetAmount(match fee { Fee::Static => eth::U256::default(), @@ -193,16 +43,9 @@ impl Fulfillment { }), }; - let protocol_fee = match order.side { - order::Side::Buy => order::TargetAmount::default(), - order::Side::Sell => order::TargetAmount(protocol_fee.0), - }; - match order.partial { - order::Partial::Yes { available } => { - executed + surplus_fee + protocol_fee <= available - } - order::Partial::No => executed + surplus_fee + protocol_fee == order.target(), + order::Partial::Yes { available } => executed + fee <= available, + order::Partial::No => executed + fee == order.target(), } }; @@ -218,7 +61,6 @@ impl Fulfillment { order, executed, fee, - protocol_fee, }) } else { Err(InvalidFullfilment) @@ -238,7 +80,7 @@ impl Fulfillment { pub fn scoring_fee(&self) -> order::SellAmount { match self.fee { Fee::Static => self.order.fee.solver, - Fee::Dynamic(fee) => fee + self.protocol_fee, + Fee::Dynamic(fee) => fee, } } @@ -247,10 +89,15 @@ impl Fulfillment { pub fn fee(&self) -> order::SellAmount { match self.fee { Fee::Static => self.order.fee.user, - Fee::Dynamic(fee) => fee + self.protocol_fee, + Fee::Dynamic(fee) => fee, } } + /// Returns the raw form of the fee + pub fn raw_fee(&self) -> Fee { + self.fee + } + /// The effective amount that left the user's wallet including all fees. pub fn sell_amount( &self, @@ -357,348 +204,3 @@ pub enum ExecutionError { #[error("missing clearing price for {0:?}")] ClearingPriceMissing(eth::TokenAddress), } - -mod tests { - use { - super::*, - crate::{ - domain::competition::order::{ - signature::Scheme, - AppData, - BuyTokenBalance, - FeePolicy, - SellTokenBalance, - Signature, - TargetAmount, - }, - util, - }, - primitive_types::{H160, U256}, - std::str::FromStr, - }; - - #[test] - fn test_fulfillment_sell_limit_order_fok() { - // https://explorer.cow.fi/orders/0xef6de27933bde867c768ead05d34a08c806d35b89f6bea565bdeb40108265e9a6f419390da10911abd1e1c962b569312a9c9c7b1658a2936?tab=overview - let order = competition::Order { - uid: Default::default(), - side: order::Side::Sell, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xba3335588d9403515223f109edc4eb7269a9ab5d").unwrap(), - )), - amount: eth::TokenAmount(778310860032541096349039u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), - )), - amount: eth::TokenAmount(4166666666666666666u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::No, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.25?id=m8dnoowB4Ql8nk7a5ber - let uniform_sell_price = eth::U256::from(913320970421237626580182u128); - let uniform_buy_price = eth::U256::from(4149866666666666668u128); - let executed = order::TargetAmount(4149866666666666668u128.into()); - let fee = Fee::Dynamic(order::SellAmount(16799999999999998u128.into())); - let fulfillment = Fulfillment::new( - order.clone(), - executed, - fee, - uniform_sell_price, - uniform_buy_price, - ) - .unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((16799999999999998u128 + 306723471216604081u128).into()) - ); - // executed amount reduced by protocol fee - assert_eq!( - fulfillment.executed(), - U256::from(3843143195450062587u128).into() - ); // 4149866666666666668 - 306723471216604081 - } - - #[test] - pub fn test_fulfillment_buy_limit_order_fok() { - // https://explorer.cow.fi/orders/0xc9096a3dbfb1f661e65ecc14644adec6bd8e385ae818aa73181def24996affb589e4042fd85e857e81a4fa89831b1f5ad4f384b7659357d7?tab=overview - let order = competition::Order { - uid: Default::default(), - side: order::Side::Buy, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), - )), - amount: eth::TokenAmount(170000000000000000u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab").unwrap(), - )), - amount: eth::TokenAmount(1781433576205823004786u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::No, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=cYSDo4wBlutGF6Gybl6x - let uniform_sell_price = eth::U256::from(7213317128720734077u128); - let uniform_buy_price = eth::U256::from(74745150907421124481191u128); - let executed = order::TargetAmount(170000000000000000u128.into()); - let fee = Fee::Dynamic(order::SellAmount(19868323826701104280u128.into())); - let fulfillment = Fulfillment::new( - order.clone(), - executed, - fee, - uniform_sell_price, - uniform_buy_price, - ) - .unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((19868323826701104280u128 + 3684441086061450u128).into()) - ); - // executed amount same as before - assert_eq!(fulfillment.executed(), executed); - } - - #[test] - fn test_fulfillment_sell_limit_order_partial() { - // https://explorer.cow.fi/orders/0x1a146dba48512326c647aae1ce511206b373b151e1b9ada9772c313e7d24ec2e0960da039bb8151cacfef620476e8baf34bd95656594209e?tab=overview - // 3 fullfillments - // - // 1. tx hash 0xbc95b97d09a62e6a68b15a8dfd4655a6e25d100ce0dd98a6a43e3b7eac9951cc - // - // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=W-uxp4wBlutGF6GyxkCq - let order1 = competition::Order { - uid: Default::default(), - side: order::Side::Sell, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), - )), - amount: eth::TokenAmount(136363636363636u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), - amount: eth::TokenAmount(9000000000u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::Yes { - available: TargetAmount(9000000000u128.into()), - }, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - let uniform_sell_price = eth::U256::from(452471455796126723289489746u128); - let uniform_buy_price = eth::U256::from(29563373796548615411833u128); - let executed = order::TargetAmount(1746031488u128.into()); - let fee = Fee::Dynamic(order::SellAmount(11566733u128.into())); - let fulfillment = Fulfillment::new( - order1.clone(), - executed, - fee, - uniform_sell_price, - uniform_buy_price, - ) - .unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((11566733u128 + 3037322u128).into()) - ); - // executed amount reduced by protocol fee - assert_eq!(fulfillment.executed(), U256::from(1742994166u128).into()); // 1746031488 - 3037322 - - // 2. tx hash 0x2f9b928182649aad2eaf04361fff1aff3cb8d37e4988c952aed49465eff01c9e - // - // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=uvXcp4wB4Ql8nk7aQgeZ - - let order2 = competition::Order { - uid: Default::default(), - side: order::Side::Sell, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), - )), - amount: eth::TokenAmount(136363636363636u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), - amount: eth::TokenAmount(9000000000u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::Yes { - available: TargetAmount(7242401779u128.into()), - }, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - let uniform_sell_price = eth::U256::from(49331008874302634851980418220032u128); - let uniform_buy_price = eth::U256::from(3204738565525085525012119552u128); - let executed = order::TargetAmount(2887238741u128.into()); - let fee = Fee::Dynamic(order::SellAmount(27827963u128.into())); - let fulfillment = Fulfillment::new( - order2.clone(), - executed, - fee, - uniform_sell_price, - uniform_buy_price, - ) - .unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((27827963u128 + 8965365u128).into()) - ); - // executed amount reduced by protocol fee - assert_eq!(fulfillment.executed(), U256::from(2878273376u128).into()); // 2887238741 - 8965365 - - // 3. 0x813dab5983fd3643e1ce3e7efbdbfe1ca8c41419bcfaf1e898e067e37c455d75 - // - // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=xPXdp4wB4Ql8nk7a8ert - - let order3 = competition::Order { - uid: Default::default(), - side: order::Side::Sell, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), - )), - amount: eth::TokenAmount(136363636363636u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), - amount: eth::TokenAmount(9000000000u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::Yes { - available: TargetAmount(4327335075u128.into()), - }, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - let uniform_sell_price = eth::U256::from(65841033847428u128); - let uniform_buy_price = eth::U256::from(4302554937u128); - let executed = order::TargetAmount(4302554937u128.into()); - let fee = Fee::Dynamic(order::SellAmount(24780138u128.into())); - let fulfillment = Fulfillment::new( - order3.clone(), - executed, - fee, - uniform_sell_price, - uniform_buy_price, - ) - .unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((24780138u128 + 8996762u128).into()) - ); - // executed amount reduced by protocol fee - assert_eq!(fulfillment.executed(), U256::from(4293558175u128).into()); // 4302554937 - 8996762 - } - - #[test] - fn test_checked_sub() { - assert_eq!(U256::from(1u128).checked_sub(U256::from(2u128)), None); - assert_eq!( - U256::from(2u128).checked_sub(U256::from(1u128)), - Some(U256::from(1u128)) - ); - } -} diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index 9f505ad6ea..a632862098 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -37,12 +37,7 @@ impl Solutions { "invalid order UID specified in fulfillment" ))? .clone(); - - let uniform_sell_price = - solution.prices[&order.sell.token.wrap(weth).into()]; - let uniform_buy_price = - solution.prices[&order.buy.token.wrap(weth).into()]; - + competition::solution::trade::Fulfillment::new( order, fulfillment.executed_amount.into(), @@ -52,8 +47,6 @@ impl Solutions { ), None => competition::solution::trade::Fee::Static, }, - uniform_sell_price, - uniform_buy_price, ) .map(competition::solution::Trade::Fulfillment) .map_err( From 121cb9518b1a66a5049a7147b9c654855b7b994d Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 16:20:54 +0100 Subject: [PATCH 15/35] apply fees --- .../src/domain/competition/solution/fee.rs | 14 +++++----- .../src/domain/competition/solution/mod.rs | 26 ++++++++++++++++++- .../driver/src/infra/solver/dto/solution.rs | 2 +- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 970f3807ed..a72dfa1c2d 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -15,8 +15,8 @@ use { }; impl Fulfillment { - pub fn apply_protocol_fee( - self, + pub fn add_protocol_fee( + &self, uniform_sell_price: eth::U256, uniform_buy_price: eth::U256, ) -> Result { @@ -278,7 +278,7 @@ mod tests { assert_eq!(fulfillment.executed(), executed); let fulfillment = fulfillment - .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .add_protocol_fee(uniform_sell_price, uniform_buy_price) .unwrap(); // fee contains protocol fee assert_eq!( @@ -346,7 +346,7 @@ mod tests { assert_eq!(fulfillment.executed(), executed); let fulfillment = fulfillment - .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .add_protocol_fee(uniform_sell_price, uniform_buy_price) .unwrap(); // fee contains protocol fee assert_eq!( @@ -414,7 +414,7 @@ mod tests { assert_eq!(fulfillment.executed(), executed); let fulfillment = fulfillment - .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .add_protocol_fee(uniform_sell_price, uniform_buy_price) .unwrap(); // fee contains protocol fee assert_eq!( @@ -477,7 +477,7 @@ mod tests { assert_eq!(fulfillment.executed(), executed); let fulfillment = fulfillment - .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .add_protocol_fee(uniform_sell_price, uniform_buy_price) .unwrap(); // fee contains protocol fee assert_eq!( @@ -540,7 +540,7 @@ mod tests { assert_eq!(fulfillment.executed(), executed); let fulfillment = fulfillment - .apply_protocol_fee(uniform_sell_price, uniform_buy_price) + .add_protocol_fee(uniform_sell_price, uniform_buy_price) .unwrap(); // fee contains protocol fee assert_eq!( diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index b1820b8eaa..9c86909b6e 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -67,7 +67,7 @@ impl Solution { solution.clearing_price(trade.order().sell.token).is_some() && solution.clearing_price(trade.order().buy.token).is_some() }) { - Ok(solution) + Ok(solution.with_protocol_fees()) } else { Err(InvalidClearingPrices) } @@ -178,6 +178,30 @@ impl Solution { Settlement::encode(self, auction, eth, simulator).await } + pub fn with_protocol_fees(self) -> Self { + let trades = self + .trades + .into_iter() + .map(|trade| match &trade { + Trade::Fulfillment(fulfillment) => match fulfillment.order().kind { + order::Kind::Market | order::Kind::Limit { .. } => { + let uniform_sell_price = + self.prices[&fulfillment.order().sell.token.wrap(self.weth)]; + let uniform_buy_price = + self.prices[&fulfillment.order().buy.token.wrap(self.weth)]; + fulfillment + .add_protocol_fee(uniform_sell_price, uniform_buy_price) + .map(Trade::Fulfillment) + .unwrap_or(trade) + } + order::Kind::Liquidity => trade, + }, + Trade::Jit(_) => trade, + }) + .collect(); + Self { trades, ..self } + } + /// Token prices settled by this solution, expressed using an arbitrary /// reference unit chosen by the solver. These values are only /// meaningful in relation to each others. diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index a632862098..4986095e33 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -37,7 +37,7 @@ impl Solutions { "invalid order UID specified in fulfillment" ))? .clone(); - + competition::solution::trade::Fulfillment::new( order, fulfillment.executed_amount.into(), From a59be019030bfb90a787d49131cc603b8b7315a2 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 27 Dec 2023 16:28:19 +0100 Subject: [PATCH 16/35] reverts --- .../driver/src/domain/competition/order/mod.rs | 16 ---------------- .../src/domain/competition/solution/fee.rs | 13 ++++++------- .../src/domain/competition/solution/trade.rs | 16 +++++++++------- crates/driver/src/infra/solver/dto/solution.rs | 4 ++-- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/crates/driver/src/domain/competition/order/mod.rs b/crates/driver/src/domain/competition/order/mod.rs index 7c4ea1bed9..4135ab54bf 100644 --- a/crates/driver/src/domain/competition/order/mod.rs +++ b/crates/driver/src/domain/competition/order/mod.rs @@ -68,14 +68,6 @@ impl From for eth::U256 { } } -impl std::ops::Add for SellAmount { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - /// An amount denominated in the sell token for [`Side::Sell`] [`Order`]s, or in /// the buy token for [`Side::Buy`] [`Order`]s. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -105,14 +97,6 @@ impl From for eth::TokenAmount { } } -impl std::ops::Add for TargetAmount { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - /// Order fee denominated in the sell token. #[derive(Debug, Default, Clone, Copy)] pub struct Fee { diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index a72dfa1c2d..3b4b6de0c4 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -4,7 +4,7 @@ //! solver. use { - super::trade::{Fee, Fulfillment, InvalidFullfilment}, + super::trade::{Fee, Fulfillment, InvalidExecutedAmount}, crate::domain::{ competition::{ order, @@ -19,7 +19,7 @@ impl Fulfillment { &self, uniform_sell_price: eth::U256, uniform_buy_price: eth::U256, - ) -> Result { + ) -> Result { let order = self.order().clone(); let executed = self.executed(); let surplus_fee = match self.raw_fee() { @@ -97,13 +97,12 @@ impl Fulfillment { tracing::warn!(?order.uid, "failed to calculate protocol fee"); acc } - }) - .into(); + }); // Increase the fee by the protocol fee let fee = match self.raw_fee() { Fee::Static => Fee::Static, - Fee::Dynamic(fee) => Fee::Dynamic(fee + protocol_fee), + Fee::Dynamic(fee) => Fee::Dynamic((fee.0 + protocol_fee).into()), }; // Adjust the executed amount by the protocol fee. This is because solvers are @@ -114,8 +113,8 @@ impl Fulfillment { order::Side::Sell => order::TargetAmount( executed .0 - .checked_sub(protocol_fee.0) - .ok_or(InvalidFullfilment)?, + .checked_sub(protocol_fee) + .ok_or(InvalidExecutedAmount)?, ), }; diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 27306618de..99c71ef818 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -30,7 +30,7 @@ impl Fulfillment { order: competition::Order, executed: order::TargetAmount, fee: Fee, - ) -> Result { + ) -> Result { // If the order is partial, the total executed amount can be smaller than // the target amount. Otherwise, the executed amount must be equal to the target // amount. @@ -44,8 +44,10 @@ impl Fulfillment { }; match order.partial { - order::Partial::Yes { available } => executed + fee <= available, - order::Partial::No => executed + fee == order.target(), + order::Partial::Yes { available } => { + order::TargetAmount(executed.0 + fee.0) <= available + } + order::Partial::No => order::TargetAmount(executed.0 + fee.0) == order.target(), } }; @@ -63,7 +65,7 @@ impl Fulfillment { fee, }) } else { - Err(InvalidFullfilment) + Err(InvalidExecutedAmount) } } @@ -159,7 +161,7 @@ impl Jit { pub fn new( order: order::Jit, executed: order::TargetAmount, - ) -> Result { + ) -> Result { // If the order is partially fillable, the executed amount can be smaller than // the target amount. Otherwise, the executed amount must be equal to the target // amount. @@ -171,7 +173,7 @@ impl Jit { if is_valid { Ok(Self { order, executed }) } else { - Err(InvalidFullfilment) + Err(InvalidExecutedAmount) } } @@ -195,7 +197,7 @@ pub struct Execution { #[derive(Debug, thiserror::Error)] #[error("invalid executed amount")] -pub struct InvalidFullfilment; +pub struct InvalidExecutedAmount; #[derive(Debug, thiserror::Error)] pub enum ExecutionError { diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index 4986095e33..8f9dd40ba8 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -50,7 +50,7 @@ impl Solutions { ) .map(competition::solution::Trade::Fulfillment) .map_err( - |competition::solution::trade::InvalidFullfilment| { + |competition::solution::trade::InvalidExecutedAmount| { super::Error("invalid trade fulfillment") }, ) @@ -116,7 +116,7 @@ impl Solutions { jit.executed_amount.into(), ) .map_err( - |competition::solution::trade::InvalidFullfilment| { + |competition::solution::trade::InvalidExecutedAmount| { super::Error("invalid executed amount in JIT order") }, )?, From 60d200ae1bbbfaa926c422540a970f45ec98042d Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 28 Dec 2023 13:49:39 +0100 Subject: [PATCH 17/35] further refactor --- .../src/domain/competition/solution/fee.rs | 232 ++++++++---------- 1 file changed, 102 insertions(+), 130 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 3b4b6de0c4..ec93d99103 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -21,36 +21,22 @@ impl Fulfillment { uniform_buy_price: eth::U256, ) -> Result { let order = self.order().clone(); - let executed = self.executed(); - let surplus_fee = match self.raw_fee() { - Fee::Static => eth::U256::default(), - Fee::Dynamic(fee) => fee.0, - }; let protocol_fee = order .fee_policies .iter() .map(|fee_policy| { - match (fee_policy, order.side) { - ( - FeePolicy::PriceImprovement { - factor, - max_volume_factor, - }, - Side::Buy, - ) => { - let price_improvement_fee = price_improvement_fee_for_buy( - order.sell.amount.0, - order.buy.amount.0, - executed.0, - surplus_fee, + match fee_policy { + FeePolicy::PriceImprovement { + factor, + max_volume_factor, + } => { + let price_improvement_fee = self.price_improvement_fee( uniform_sell_price, uniform_buy_price, *factor, )?; - let max_volume_fee = volume_fee_for_buy( - executed.0, - surplus_fee, + let max_volume_fee = self.volume_fee( uniform_sell_price, uniform_buy_price, *max_volume_factor, @@ -58,36 +44,8 @@ impl Fulfillment { // take the smaller of the two Some(std::cmp::min(price_improvement_fee, max_volume_fee)) } - ( - FeePolicy::PriceImprovement { - factor, - max_volume_factor, - }, - Side::Sell, - ) => { - let price_improvement_fee = price_improvement_fee_for_sell( - order.sell.amount.0, - order.buy.amount.0, - executed.0, - surplus_fee, - uniform_sell_price, - uniform_buy_price, - *factor, - )?; - let max_volume_fee = - volume_fee_for_sell(executed.0, surplus_fee, *max_volume_factor)?; - // take the smaller of the two - Some(std::cmp::min(price_improvement_fee, max_volume_fee)) - } - (FeePolicy::Volume { factor }, Side::Buy) => volume_fee_for_buy( - executed.0, - surplus_fee, - uniform_sell_price, - uniform_buy_price, - *factor, - ), - (FeePolicy::Volume { factor }, Side::Sell) => { - volume_fee_for_sell(executed.0, surplus_fee, *factor) + FeePolicy::Volume { factor } => { + self.volume_fee(uniform_sell_price, uniform_buy_price, *factor) } } }) @@ -109,9 +67,9 @@ impl Fulfillment { // unaware of the protocol fee that driver introduces and they only account // for the surplus fee. let executed = match order.side { - order::Side::Buy => executed, + order::Side::Buy => self.executed(), order::Side::Sell => order::TargetAmount( - executed + self.executed() .0 .checked_sub(protocol_fee) .ok_or(InvalidExecutedAmount)?, @@ -120,85 +78,99 @@ impl Fulfillment { Fulfillment::new(order, executed, fee) } -} - -fn price_improvement_fee_for_buy( - sell_amount: eth::U256, - buy_amount: eth::U256, - executed_buy_amount: eth::U256, - surplus_fee: eth::U256, - uniform_sell_price: eth::U256, - uniform_buy_price: eth::U256, - factor: f64, -) -> Option { - // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` - let executed_sell_amount = executed_buy_amount - .checked_mul(uniform_buy_price)? - .checked_div(uniform_sell_price)?; - // Sell slightly more `sell_token` to capture the `surplus_fee` - let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; - // Scale to support partially fillable orders - let limit_sell_amount = sell_amount - .checked_mul(executed_buy_amount)? - .checked_div(buy_amount)?; - // Remaining surplus after fees - let surplus = limit_sell_amount - .checked_sub(executed_sell_amount_with_fee) - .unwrap_or(eth::U256::zero()); - Some(surplus.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) -} - -fn price_improvement_fee_for_sell( - sell_amount: eth::U256, - buy_amount: eth::U256, - executed_sell_amount: eth::U256, - surplus_fee: eth::U256, - uniform_sell_price: eth::U256, - uniform_buy_price: eth::U256, - factor: f64, -) -> Option { - // How much `buy_token` we get for `executed` amount of `sell_token` - let executed_buy_amount = executed_sell_amount - .checked_mul(uniform_sell_price)? - .checked_div(uniform_buy_price)?; - let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; - // Scale to support partially fillable orders - let limit_buy_amount = buy_amount - .checked_mul(executed_sell_amount_with_fee)? - .checked_div(sell_amount)?; - // Remaining surplus after fees - let surplus = executed_buy_amount - .checked_sub(limit_buy_amount) - .unwrap_or(eth::U256::zero()); - let surplus_in_sell_token = surplus - .checked_mul(uniform_buy_price)? - .checked_div(uniform_sell_price)?; - Some(surplus_in_sell_token.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) -} -fn volume_fee_for_buy( - executed_buy_amount: eth::U256, - surplus_fee: eth::U256, - uniform_sell_price: eth::U256, - uniform_buy_price: eth::U256, - factor: f64, -) -> Option { - // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` - let executed_sell_amount = executed_buy_amount - .checked_mul(uniform_buy_price)? - .checked_div(uniform_sell_price)?; - // Sell slightly more `sell_token` to capture the `surplus_fee` - let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; - Some(executed_sell_amount_with_fee.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) -} + fn price_improvement_fee( + &self, + uniform_sell_price: eth::U256, + uniform_buy_price: eth::U256, + factor: f64, + ) -> Option { + let sell_amount = self.order().sell.amount.0; + let buy_amount = self.order().buy.amount.0; + let executed = self.executed().0; + let surplus_fee = match self.raw_fee() { + Fee::Static => eth::U256::zero(), + Fee::Dynamic(fee) => fee.0, + }; + match self.order().side { + Side::Buy => { + // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` + let executed_sell_amount = executed + .checked_mul(uniform_buy_price)? + .checked_div(uniform_sell_price)?; + // Sell slightly more `sell_token` to capture the `surplus_fee` + let executed_sell_amount_with_fee = + executed_sell_amount.checked_add(surplus_fee)?; + // Scale to support partially fillable orders + let limit_sell_amount = + sell_amount.checked_mul(executed)?.checked_div(buy_amount)?; + // Remaining surplus after fees + let surplus = limit_sell_amount + .checked_sub(executed_sell_amount_with_fee) + .unwrap_or(eth::U256::zero()); + Some(surplus.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) + } + Side::Sell => { + // How much `buy_token` we get for `executed` amount of `sell_token` + let executed_buy_amount = executed + .checked_mul(uniform_sell_price)? + .checked_div(uniform_buy_price)?; + let executed_sell_amount_with_fee = executed.checked_add(surplus_fee)?; + // Scale to support partially fillable orders + let limit_buy_amount = buy_amount + .checked_mul(executed_sell_amount_with_fee)? + .checked_div(sell_amount)?; + // Remaining surplus after fees + let surplus = executed_buy_amount + .checked_sub(limit_buy_amount) + .unwrap_or(eth::U256::zero()); + let surplus_in_sell_token = surplus + .checked_mul(uniform_buy_price)? + .checked_div(uniform_sell_price)?; + Some( + surplus_in_sell_token.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? + / 100, + ) + } + } + } -fn volume_fee_for_sell( - executed_sell_amount: eth::U256, - surplus_fee: eth::U256, - factor: f64, -) -> Option { - let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; - Some(executed_sell_amount_with_fee.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) + fn volume_fee( + &self, + uniform_sell_price: eth::U256, + uniform_buy_price: eth::U256, + factor: f64, + ) -> Option { + let executed = self.executed().0; + let surplus_fee = match self.raw_fee() { + Fee::Static => eth::U256::zero(), + Fee::Dynamic(fee) => fee.0, + }; + match self.order().side { + Side::Buy => { + // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` + let executed_sell_amount = executed + .checked_mul(uniform_buy_price)? + .checked_div(uniform_sell_price)?; + // Sell slightly more `sell_token` to capture the `surplus_fee` + let executed_sell_amount_with_fee = + executed_sell_amount.checked_add(surplus_fee)?; + Some( + executed_sell_amount_with_fee + .checked_mul(eth::U256::from_f64_lossy(factor * 100.))? + / 100, + ) + } + Side::Sell => { + let executed_sell_amount_with_fee = executed.checked_add(surplus_fee)?; + Some( + executed_sell_amount_with_fee + .checked_mul(eth::U256::from_f64_lossy(factor * 100.))? + / 100, + ) + } + } + } } mod tests { From 15bccce3cf904e31fc9c2c795c98a4dcae20215c Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 28 Dec 2023 15:59:12 +0100 Subject: [PATCH 18/35] refactor clearing prices --- .../src/domain/competition/solution/fee.rs | 193 +++++++++--------- .../src/domain/competition/solution/mod.rs | 11 +- 2 files changed, 105 insertions(+), 99 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index ec93d99103..2adf26452f 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -1,7 +1,26 @@ //! Applies the protocol fee to the solution received from the solver. //! -//! Protocol fee is applied by increasing already existing fee determined by the -//! solver. +//! Solvers respond differently for the sell and buy orders. +//! +//! EXAMPLES: +//! +//! SELL ORDER +//! Selling 1 WETH for at least `x` amount of USDC. Solvers respond with +//! Fee = 0.05 WETH +//! Executed = 0.95 WETH +//! +//! This response is adjusted by the protocol fee of 0.1 WETH: +//! Fee = 0.05 WETH + 0.1 WETH = 0.15 WETH +//! Executed = 0.95 WETH - 0.1 WETH = 0.85 WETH +//! +//! BUY ORDER +//! Buying 1 WETH for at most `x` amount of USDC. Solvers respond with +//! Fee = 0.05 WETH +//! Executed = 1 WETH +//! +//! This response is adjusted by the protocol fee of 0.1 WETH: +//! Fee = 0.05 WETH + 0.1 WETH = 0.15 WETH +//! Executed = 1 WETH use { super::trade::{Fee, Fulfillment, InvalidExecutedAmount}, @@ -15,47 +34,9 @@ use { }; impl Fulfillment { - pub fn add_protocol_fee( - &self, - uniform_sell_price: eth::U256, - uniform_buy_price: eth::U256, - ) -> Result { - let order = self.order().clone(); - - let protocol_fee = order - .fee_policies - .iter() - .map(|fee_policy| { - match fee_policy { - FeePolicy::PriceImprovement { - factor, - max_volume_factor, - } => { - let price_improvement_fee = self.price_improvement_fee( - uniform_sell_price, - uniform_buy_price, - *factor, - )?; - let max_volume_fee = self.volume_fee( - uniform_sell_price, - uniform_buy_price, - *max_volume_factor, - )?; - // take the smaller of the two - Some(std::cmp::min(price_improvement_fee, max_volume_fee)) - } - FeePolicy::Volume { factor } => { - self.volume_fee(uniform_sell_price, uniform_buy_price, *factor) - } - } - }) - .fold(eth::U256::zero(), |acc, fee| match fee { - Some(fee) => acc + fee, - None => { - tracing::warn!(?order.uid, "failed to calculate protocol fee"); - acc - } - }); + /// Applies the protocol fee to the existing fulfillment. + pub fn with_protocol_fee(&self, prices: ClearingPrices) -> Result { + let protocol_fee = self.protocol_fee(prices)?; // Increase the fee by the protocol fee let fee = match self.raw_fee() { @@ -63,9 +44,10 @@ impl Fulfillment { Fee::Dynamic(fee) => Fee::Dynamic((fee.0 + protocol_fee).into()), }; - // Adjust the executed amount by the protocol fee. This is because solvers are + // Reduce the executed amount by the protocol fee. This is because solvers are // unaware of the protocol fee that driver introduces and they only account - // for the surplus fee. + // for their own fee. + let order = self.order().clone(); let executed = match order.side { order::Side::Buy => self.executed(), order::Side::Sell => order::TargetAmount( @@ -79,12 +61,37 @@ impl Fulfillment { Fulfillment::new(order, executed, fee) } - fn price_improvement_fee( - &self, - uniform_sell_price: eth::U256, - uniform_buy_price: eth::U256, - factor: f64, - ) -> Option { + fn protocol_fee(&self, prices: ClearingPrices) -> Result { + let mut protocol_fee = eth::U256::zero(); + for fee_policy in self.order().fee_policies.iter() { + match fee_policy { + FeePolicy::PriceImprovement { + factor, + max_volume_factor, + } => { + let price_improvement_fee = self + .price_improvement_fee(prices, *factor) + .ok_or(InvalidExecutedAmount)?; + let max_volume_fee = self + .volume_fee(prices, *max_volume_factor) + .ok_or(InvalidExecutedAmount)?; + // take the smaller of the two + protocol_fee = protocol_fee + .checked_add(std::cmp::min(price_improvement_fee, max_volume_fee)) + .ok_or(InvalidExecutedAmount)?; + } + FeePolicy::Volume { factor } => { + let fee = self + .volume_fee(prices, *factor) + .ok_or(InvalidExecutedAmount)?; + protocol_fee = protocol_fee.checked_add(fee).ok_or(InvalidExecutedAmount)?; + } + } + } + Ok(protocol_fee) + } + + fn price_improvement_fee(&self, prices: ClearingPrices, factor: f64) -> Option { let sell_amount = self.order().sell.amount.0; let buy_amount = self.order().buy.amount.0; let executed = self.executed().0; @@ -95,9 +102,8 @@ impl Fulfillment { match self.order().side { Side::Buy => { // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` - let executed_sell_amount = executed - .checked_mul(uniform_buy_price)? - .checked_div(uniform_sell_price)?; + let executed_sell_amount = + executed.checked_mul(prices.buy)?.checked_div(prices.sell)?; // Sell slightly more `sell_token` to capture the `surplus_fee` let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; @@ -112,9 +118,8 @@ impl Fulfillment { } Side::Sell => { // How much `buy_token` we get for `executed` amount of `sell_token` - let executed_buy_amount = executed - .checked_mul(uniform_sell_price)? - .checked_div(uniform_buy_price)?; + let executed_buy_amount = + executed.checked_mul(prices.sell)?.checked_div(prices.buy)?; let executed_sell_amount_with_fee = executed.checked_add(surplus_fee)?; // Scale to support partially fillable orders let limit_buy_amount = buy_amount @@ -124,9 +129,8 @@ impl Fulfillment { let surplus = executed_buy_amount .checked_sub(limit_buy_amount) .unwrap_or(eth::U256::zero()); - let surplus_in_sell_token = surplus - .checked_mul(uniform_buy_price)? - .checked_div(uniform_sell_price)?; + let surplus_in_sell_token = + surplus.checked_mul(prices.buy)?.checked_div(prices.sell)?; Some( surplus_in_sell_token.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100, @@ -135,12 +139,7 @@ impl Fulfillment { } } - fn volume_fee( - &self, - uniform_sell_price: eth::U256, - uniform_buy_price: eth::U256, - factor: f64, - ) -> Option { + fn volume_fee(&self, prices: ClearingPrices, factor: f64) -> Option { let executed = self.executed().0; let surplus_fee = match self.raw_fee() { Fee::Static => eth::U256::zero(), @@ -149,9 +148,8 @@ impl Fulfillment { match self.order().side { Side::Buy => { // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` - let executed_sell_amount = executed - .checked_mul(uniform_buy_price)? - .checked_div(uniform_sell_price)?; + let executed_sell_amount = + executed.checked_mul(prices.buy)?.checked_div(prices.sell)?; // Sell slightly more `sell_token` to capture the `surplus_fee` let executed_sell_amount_with_fee = executed_sell_amount.checked_add(surplus_fee)?; @@ -173,6 +171,13 @@ impl Fulfillment { } } +/// Uniform clearing prices at which the trade was executed. +#[derive(Debug, Clone, Copy)] +pub struct ClearingPrices { + pub sell: eth::U256, + pub buy: eth::U256, +} + mod tests { use { super::*, @@ -235,8 +240,10 @@ mod tests { }; // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.25?id=m8dnoowB4Ql8nk7a5ber - let uniform_sell_price = eth::U256::from(913320970421237626580182u128); - let uniform_buy_price = eth::U256::from(4149866666666666668u128); + let prices = ClearingPrices { + sell: eth::U256::from(913320970421237626580182u128), + buy: eth::U256::from(4149866666666666668u128), + }; let executed = order::TargetAmount(4149866666666666668u128.into()); let fee = Fee::Dynamic(order::SellAmount(16799999999999998u128.into())); let fulfillment = Fulfillment::new(order.clone(), executed, fee).unwrap(); @@ -248,9 +255,7 @@ mod tests { // executed amount before protocol fee assert_eq!(fulfillment.executed(), executed); - let fulfillment = fulfillment - .add_protocol_fee(uniform_sell_price, uniform_buy_price) - .unwrap(); + let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); // fee contains protocol fee assert_eq!( fulfillment.fee(), @@ -303,8 +308,10 @@ mod tests { }; // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=cYSDo4wBlutGF6Gybl6x - let uniform_sell_price = eth::U256::from(7213317128720734077u128); - let uniform_buy_price = eth::U256::from(74745150907421124481191u128); + let prices = ClearingPrices { + sell: eth::U256::from(7213317128720734077u128), + buy: eth::U256::from(74745150907421124481191u128), + }; let executed = order::TargetAmount(170000000000000000u128.into()); let fee = Fee::Dynamic(order::SellAmount(19868323826701104280u128.into())); let fulfillment = Fulfillment::new(order.clone(), executed, fee).unwrap(); @@ -316,9 +323,7 @@ mod tests { // executed amount before protocol fee assert_eq!(fulfillment.executed(), executed); - let fulfillment = fulfillment - .add_protocol_fee(uniform_sell_price, uniform_buy_price) - .unwrap(); + let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); // fee contains protocol fee assert_eq!( fulfillment.fee(), @@ -374,8 +379,10 @@ mod tests { }, }; - let uniform_sell_price = eth::U256::from(452471455796126723289489746u128); - let uniform_buy_price = eth::U256::from(29563373796548615411833u128); + let prices = ClearingPrices { + sell: eth::U256::from(452471455796126723289489746u128), + buy: eth::U256::from(29563373796548615411833u128), + }; let executed = order::TargetAmount(1746031488u128.into()); let fee = Fee::Dynamic(order::SellAmount(11566733u128.into())); let fulfillment = Fulfillment::new(order1.clone(), executed, fee).unwrap(); @@ -384,9 +391,7 @@ mod tests { // executed amount before protocol fee assert_eq!(fulfillment.executed(), executed); - let fulfillment = fulfillment - .add_protocol_fee(uniform_sell_price, uniform_buy_price) - .unwrap(); + let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); // fee contains protocol fee assert_eq!( fulfillment.fee(), @@ -437,8 +442,10 @@ mod tests { }, }; - let uniform_sell_price = eth::U256::from(49331008874302634851980418220032u128); - let uniform_buy_price = eth::U256::from(3204738565525085525012119552u128); + let prices = ClearingPrices { + sell: eth::U256::from(49331008874302634851980418220032u128), + buy: eth::U256::from(3204738565525085525012119552u128), + }; let executed = order::TargetAmount(2887238741u128.into()); let fee = Fee::Dynamic(order::SellAmount(27827963u128.into())); let fulfillment = Fulfillment::new(order2.clone(), executed, fee).unwrap(); @@ -447,9 +454,7 @@ mod tests { // executed amount before protocol fee assert_eq!(fulfillment.executed(), executed); - let fulfillment = fulfillment - .add_protocol_fee(uniform_sell_price, uniform_buy_price) - .unwrap(); + let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); // fee contains protocol fee assert_eq!( fulfillment.fee(), @@ -500,8 +505,10 @@ mod tests { }, }; - let uniform_sell_price = eth::U256::from(65841033847428u128); - let uniform_buy_price = eth::U256::from(4302554937u128); + let prices = ClearingPrices { + sell: eth::U256::from(65841033847428u128), + buy: eth::U256::from(4302554937u128), + }; let executed = order::TargetAmount(4302554937u128.into()); let fee = Fee::Dynamic(order::SellAmount(24780138u128.into())); let fulfillment = Fulfillment::new(order3.clone(), executed, fee).unwrap(); @@ -510,9 +517,7 @@ mod tests { // executed amount before protocol fee assert_eq!(fulfillment.executed(), executed); - let fulfillment = fulfillment - .add_protocol_fee(uniform_sell_price, uniform_buy_price) - .unwrap(); + let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); // fee contains protocol fee assert_eq!( fulfillment.fee(), diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 9c86909b6e..188fdb6f6a 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -1,4 +1,5 @@ use { + self::fee::ClearingPrices, crate::{ boundary, domain::{ @@ -185,12 +186,12 @@ impl Solution { .map(|trade| match &trade { Trade::Fulfillment(fulfillment) => match fulfillment.order().kind { order::Kind::Market | order::Kind::Limit { .. } => { - let uniform_sell_price = - self.prices[&fulfillment.order().sell.token.wrap(self.weth)]; - let uniform_buy_price = - self.prices[&fulfillment.order().buy.token.wrap(self.weth)]; + let prices = ClearingPrices { + sell: self.prices[&fulfillment.order().sell.token.wrap(self.weth)], + buy: self.prices[&fulfillment.order().buy.token.wrap(self.weth)], + }; fulfillment - .add_protocol_fee(uniform_sell_price, uniform_buy_price) + .with_protocol_fee(prices) .map(Trade::Fulfillment) .unwrap_or(trade) } From 41d8b2fa5dc85baa480fefd988dea501ff0c7008 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 28 Dec 2023 16:45:59 +0100 Subject: [PATCH 19/35] error handling --- .../src/domain/competition/solution/fee.rs | 142 +++++++++++------- .../src/domain/competition/solution/mod.rs | 48 +++--- .../src/domain/competition/solution/trade.rs | 11 +- crates/driver/src/infra/notify/mod.rs | 1 - .../driver/src/infra/solver/dto/solution.rs | 9 +- 5 files changed, 121 insertions(+), 90 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 2adf26452f..0523987df1 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -1,23 +1,23 @@ //! Applies the protocol fee to the solution received from the solver. //! //! Solvers respond differently for the sell and buy orders. -//! +//! //! EXAMPLES: -//! +//! //! SELL ORDER //! Selling 1 WETH for at least `x` amount of USDC. Solvers respond with //! Fee = 0.05 WETH //! Executed = 0.95 WETH -//! +//! //! This response is adjusted by the protocol fee of 0.1 WETH: //! Fee = 0.05 WETH + 0.1 WETH = 0.15 WETH //! Executed = 0.95 WETH - 0.1 WETH = 0.85 WETH -//! +//! //! BUY ORDER //! Buying 1 WETH for at most `x` amount of USDC. Solvers respond with //! Fee = 0.05 WETH //! Executed = 1 WETH -//! +//! //! This response is adjusted by the protocol fee of 0.1 WETH: //! Fee = 0.05 WETH + 0.1 WETH = 0.15 WETH //! Executed = 1 WETH @@ -34,8 +34,8 @@ use { }; impl Fulfillment { - /// Applies the protocol fee to the existing fulfillment. - pub fn with_protocol_fee(&self, prices: ClearingPrices) -> Result { + /// Applies the protocol fee to the existing fulfillment creating a new one. + pub fn with_protocol_fee(&self, prices: ClearingPrices) -> Result { let protocol_fee = self.protocol_fee(prices)?; // Increase the fee by the protocol fee @@ -54,14 +54,14 @@ impl Fulfillment { self.executed() .0 .checked_sub(protocol_fee) - .ok_or(InvalidExecutedAmount)?, + .ok_or(Error::Overflow)?, ), }; - Fulfillment::new(order, executed, fee) + Fulfillment::new(order, executed, fee).map_err(Into::into) } - fn protocol_fee(&self, prices: ClearingPrices) -> Result { + fn protocol_fee(&self, prices: ClearingPrices) -> Result { let mut protocol_fee = eth::U256::zero(); for fee_policy in self.order().fee_policies.iter() { match fee_policy { @@ -69,29 +69,27 @@ impl Fulfillment { factor, max_volume_factor, } => { - let price_improvement_fee = self - .price_improvement_fee(prices, *factor) - .ok_or(InvalidExecutedAmount)?; - let max_volume_fee = self - .volume_fee(prices, *max_volume_factor) - .ok_or(InvalidExecutedAmount)?; + let price_improvement_fee = self.price_improvement_fee(prices, *factor)?; + let max_volume_fee = self.volume_fee(prices, *max_volume_factor)?; // take the smaller of the two protocol_fee = protocol_fee .checked_add(std::cmp::min(price_improvement_fee, max_volume_fee)) - .ok_or(InvalidExecutedAmount)?; + .ok_or(Error::Overflow)?; } FeePolicy::Volume { factor } => { - let fee = self - .volume_fee(prices, *factor) - .ok_or(InvalidExecutedAmount)?; - protocol_fee = protocol_fee.checked_add(fee).ok_or(InvalidExecutedAmount)?; + let fee = self.volume_fee(prices, *factor)?; + protocol_fee = protocol_fee.checked_add(fee).ok_or(Error::Overflow)?; } } } Ok(protocol_fee) } - fn price_improvement_fee(&self, prices: ClearingPrices, factor: f64) -> Option { + fn price_improvement_fee( + &self, + prices: ClearingPrices, + factor: f64, + ) -> Result { let sell_amount = self.order().sell.amount.0; let buy_amount = self.order().buy.amount.0; let executed = self.executed().0; @@ -102,44 +100,63 @@ impl Fulfillment { match self.order().side { Side::Buy => { // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` - let executed_sell_amount = - executed.checked_mul(prices.buy)?.checked_div(prices.sell)?; + let executed_sell_amount = executed + .checked_mul(prices.buy) + .ok_or(Error::Overflow)? + .checked_div(prices.sell) + .ok_or(Error::DivisionByZero)?; // Sell slightly more `sell_token` to capture the `surplus_fee` - let executed_sell_amount_with_fee = - executed_sell_amount.checked_add(surplus_fee)?; + let executed_sell_amount_with_fee = executed_sell_amount + .checked_add(surplus_fee) + .ok_or(Error::Overflow)?; // Scale to support partially fillable orders - let limit_sell_amount = - sell_amount.checked_mul(executed)?.checked_div(buy_amount)?; + let limit_sell_amount = sell_amount + .checked_mul(executed) + .ok_or(Error::Overflow)? + .checked_div(buy_amount) + .ok_or(Error::DivisionByZero)?; // Remaining surplus after fees let surplus = limit_sell_amount .checked_sub(executed_sell_amount_with_fee) .unwrap_or(eth::U256::zero()); - Some(surplus.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? / 100) + Ok(surplus + .checked_mul(eth::U256::from_f64_lossy(factor * 100.)) + .ok_or(Error::Overflow)? + / 100) } Side::Sell => { // How much `buy_token` we get for `executed` amount of `sell_token` - let executed_buy_amount = - executed.checked_mul(prices.sell)?.checked_div(prices.buy)?; - let executed_sell_amount_with_fee = executed.checked_add(surplus_fee)?; + let executed_buy_amount = executed + .checked_mul(prices.sell) + .ok_or(Error::Overflow)? + .checked_div(prices.buy) + .ok_or(Error::DivisionByZero)?; + let executed_sell_amount_with_fee = + executed.checked_add(surplus_fee).ok_or(Error::Overflow)?; // Scale to support partially fillable orders let limit_buy_amount = buy_amount - .checked_mul(executed_sell_amount_with_fee)? - .checked_div(sell_amount)?; + .checked_mul(executed_sell_amount_with_fee) + .ok_or(Error::Overflow)? + .checked_div(sell_amount) + .ok_or(Error::DivisionByZero)?; // Remaining surplus after fees let surplus = executed_buy_amount .checked_sub(limit_buy_amount) .unwrap_or(eth::U256::zero()); - let surplus_in_sell_token = - surplus.checked_mul(prices.buy)?.checked_div(prices.sell)?; - Some( - surplus_in_sell_token.checked_mul(eth::U256::from_f64_lossy(factor * 100.))? - / 100, - ) + let surplus_in_sell_token = surplus + .checked_mul(prices.buy) + .ok_or(Error::Overflow)? + .checked_div(prices.sell) + .ok_or(Error::DivisionByZero)?; + Ok(surplus_in_sell_token + .checked_mul(eth::U256::from_f64_lossy(factor * 100.)) + .ok_or(Error::Overflow)? + / 100) } } } - fn volume_fee(&self, prices: ClearingPrices, factor: f64) -> Option { + fn volume_fee(&self, prices: ClearingPrices, factor: f64) -> Result { let executed = self.executed().0; let surplus_fee = match self.raw_fee() { Fee::Static => eth::U256::zero(), @@ -148,24 +165,27 @@ impl Fulfillment { match self.order().side { Side::Buy => { // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` - let executed_sell_amount = - executed.checked_mul(prices.buy)?.checked_div(prices.sell)?; + let executed_sell_amount = executed + .checked_mul(prices.buy) + .ok_or(Error::Overflow)? + .checked_div(prices.sell) + .ok_or(Error::DivisionByZero)?; // Sell slightly more `sell_token` to capture the `surplus_fee` - let executed_sell_amount_with_fee = - executed_sell_amount.checked_add(surplus_fee)?; - Some( - executed_sell_amount_with_fee - .checked_mul(eth::U256::from_f64_lossy(factor * 100.))? - / 100, - ) + let executed_sell_amount_with_fee = executed_sell_amount + .checked_add(surplus_fee) + .ok_or(Error::Overflow)?; + Ok(executed_sell_amount_with_fee + .checked_mul(eth::U256::from_f64_lossy(factor * 100.)) + .ok_or(Error::Overflow)? + / 100) } Side::Sell => { - let executed_sell_amount_with_fee = executed.checked_add(surplus_fee)?; - Some( - executed_sell_amount_with_fee - .checked_mul(eth::U256::from_f64_lossy(factor * 100.))? - / 100, - ) + let executed_sell_amount_with_fee = + executed.checked_add(surplus_fee).ok_or(Error::Overflow)?; + Ok(executed_sell_amount_with_fee + .checked_mul(eth::U256::from_f64_lossy(factor * 100.)) + .ok_or(Error::Overflow)? + / 100) } } } @@ -178,6 +198,16 @@ pub struct ClearingPrices { pub buy: eth::U256, } +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("overflow error while calculating protocol fee")] + Overflow, + #[error("division by zero error while calculating protocol fee")] + DivisionByZero, + #[error(transparent)] + InvalidExecutedAmount(#[from] InvalidExecutedAmount), +} + mod tests { use { super::*, diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 188fdb6f6a..f9fcab025d 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -52,7 +52,7 @@ impl Solution { solver: Solver, score: SolverScore, weth: eth::WethAddress, - ) -> Result { + ) -> Result { let solution = Self { id, trades, @@ -68,9 +68,9 @@ impl Solution { solution.clearing_price(trade.order().sell.token).is_some() && solution.clearing_price(trade.order().buy.token).is_some() }) { - Ok(solution.with_protocol_fees()) + Ok(solution.with_protocol_fees()?) } else { - Err(InvalidClearingPrices) + Err(SolutionError::InvalidClearingPrices) } } @@ -179,28 +179,30 @@ impl Solution { Settlement::encode(self, auction, eth, simulator).await } - pub fn with_protocol_fees(self) -> Self { - let trades = self - .trades - .into_iter() - .map(|trade| match &trade { + pub fn with_protocol_fees(self) -> Result { + let mut trades = Vec::new(); + for trade in self.trades { + match &trade { Trade::Fulfillment(fulfillment) => match fulfillment.order().kind { order::Kind::Market | order::Kind::Limit { .. } => { let prices = ClearingPrices { sell: self.prices[&fulfillment.order().sell.token.wrap(self.weth)], buy: self.prices[&fulfillment.order().buy.token.wrap(self.weth)], }; - fulfillment - .with_protocol_fee(prices) - .map(Trade::Fulfillment) - .unwrap_or(trade) + trades.push( + fulfillment + .with_protocol_fee(prices) + .map(Trade::Fulfillment)?, + ); + } + order::Kind::Liquidity => { + trades.push(trade); } - order::Kind::Liquidity => trade, }, - Trade::Jit(_) => trade, - }) - .collect(); - Self { trades, ..self } + Trade::Jit(_) => trades.push(trade), + } + } + Ok(Self { trades, ..self }) } /// Token prices settled by this solution, expressed using an arbitrary @@ -331,8 +333,6 @@ pub enum Error { Boundary(#[from] boundary::Error), #[error("simulation error: {0:?}")] Simulation(#[from] simulator::Error), - #[error(transparent)] - Execution(#[from] trade::ExecutionError), #[error( "non bufferable tokens used: solution attempts to internalize tokens which are not trusted" )] @@ -345,6 +345,10 @@ pub enum Error { DifferentSolvers, } -#[derive(Debug, Error)] -#[error("invalid clearing prices")] -pub struct InvalidClearingPrices; +#[derive(Debug, thiserror::Error)] +pub enum SolutionError { + #[error("invalid clearing prices")] + InvalidClearingPrices, + #[error(transparent)] + FailedProtocolFee(#[from] fee::Error), +} diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 99c71ef818..3b0436379f 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -95,7 +95,8 @@ impl Fulfillment { } } - /// Returns the raw form of the fee + /// Returns the raw internal representation of the fee that contains + /// original source of the fee pub fn raw_fee(&self) -> Fee { self.fee } @@ -198,11 +199,3 @@ pub struct Execution { #[derive(Debug, thiserror::Error)] #[error("invalid executed amount")] pub struct InvalidExecutedAmount; - -#[derive(Debug, thiserror::Error)] -pub enum ExecutionError { - #[error("overflow error while calculating executed amounts")] - Overflow, - #[error("missing clearing price for {0:?}")] - ClearingPriceMissing(eth::TokenAddress), -} diff --git a/crates/driver/src/infra/notify/mod.rs b/crates/driver/src/infra/notify/mod.rs index cd96a9006c..d87d29bd57 100644 --- a/crates/driver/src/infra/notify/mod.rs +++ b/crates/driver/src/infra/notify/mod.rs @@ -77,7 +77,6 @@ pub fn encoding_failed( simulation_failed(solver, auction_id, solution_id, error, false); return; } - solution::Error::Execution(_) => return, solution::Error::FailingInternalization => return, solution::Error::DifferentSolvers => return, }; diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index 8f9dd40ba8..9692124c61 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -206,8 +206,13 @@ impl Solutions { }, weth, ) - .map_err(|competition::solution::InvalidClearingPrices| { - super::Error("invalid clearing prices") + .map_err(|err| match err { + competition::solution::SolutionError::InvalidClearingPrices => { + super::Error("invalid clearing prices") + } + competition::solution::SolutionError::FailedProtocolFee(_) => { + super::Error("failed protocol fee") + } }) }) .collect() From 0b135aaee7c29af21c53b20f0a136140ba4cd470 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 29 Dec 2023 11:44:54 +0100 Subject: [PATCH 20/35] cr fixes --- .../src/domain/competition/solution/fee.rs | 44 ++++++++++++------- .../src/domain/competition/solution/mod.rs | 9 ++-- .../src/domain/competition/solution/trade.rs | 18 ++++---- .../driver/src/infra/solver/dto/solution.rs | 2 +- crates/driver/src/tests/setup/driver.rs | 4 -- 5 files changed, 43 insertions(+), 34 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 0523987df1..d5d6d36247 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -39,9 +39,14 @@ impl Fulfillment { let protocol_fee = self.protocol_fee(prices)?; // Increase the fee by the protocol fee - let fee = match self.raw_fee() { - Fee::Static => Fee::Static, - Fee::Dynamic(fee) => Fee::Dynamic((fee.0 + protocol_fee).into()), + let fee = match self.dynamic_fee() { + None => { + if !protocol_fee.is_zero() { + return Err(Error::ProtocolFeeOnStaticOrder); + } + Fee::Static + } + Some(fee) => Fee::Dynamic((fee.0 + protocol_fee).into()), }; // Reduce the executed amount by the protocol fee. This is because solvers are @@ -62,22 +67,27 @@ impl Fulfillment { } fn protocol_fee(&self, prices: ClearingPrices) -> Result { + // TODO: support multiple fee policies + if self.order().fee_policies.len() > 1 { + return Err(Error::MultipleFeePolicies); + } + let mut protocol_fee = eth::U256::zero(); - for fee_policy in self.order().fee_policies.iter() { + for fee_policy in self.order().fee_policies.clone() { match fee_policy { FeePolicy::PriceImprovement { factor, max_volume_factor, } => { - let price_improvement_fee = self.price_improvement_fee(prices, *factor)?; - let max_volume_fee = self.volume_fee(prices, *max_volume_factor)?; + let price_improvement_fee = self.price_improvement_fee(prices, factor)?; + let max_volume_fee = self.volume_fee(prices, max_volume_factor)?; // take the smaller of the two protocol_fee = protocol_fee .checked_add(std::cmp::min(price_improvement_fee, max_volume_fee)) .ok_or(Error::Overflow)?; } FeePolicy::Volume { factor } => { - let fee = self.volume_fee(prices, *factor)?; + let fee = self.volume_fee(prices, factor)?; protocol_fee = protocol_fee.checked_add(fee).ok_or(Error::Overflow)?; } } @@ -93,10 +103,10 @@ impl Fulfillment { let sell_amount = self.order().sell.amount.0; let buy_amount = self.order().buy.amount.0; let executed = self.executed().0; - let surplus_fee = match self.raw_fee() { - Fee::Static => eth::U256::zero(), - Fee::Dynamic(fee) => fee.0, - }; + let surplus_fee = self + .dynamic_fee() + .map(|fee| fee.0) + .ok_or(Error::ProtocolFeeOnStaticOrder)?; match self.order().side { Side::Buy => { // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` @@ -158,10 +168,10 @@ impl Fulfillment { fn volume_fee(&self, prices: ClearingPrices, factor: f64) -> Result { let executed = self.executed().0; - let surplus_fee = match self.raw_fee() { - Fee::Static => eth::U256::zero(), - Fee::Dynamic(fee) => fee.0, - }; + let surplus_fee = self + .dynamic_fee() + .map(|fee| fee.0) + .ok_or(Error::ProtocolFeeOnStaticOrder)?; match self.order().side { Side::Buy => { // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` @@ -200,6 +210,10 @@ pub struct ClearingPrices { #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("orders with non solver determined gas cost fees are not supported")] + ProtocolFeeOnStaticOrder, + #[error("multiple fee policies are not supported yet")] + MultipleFeePolicies, #[error("overflow error while calculating protocol fee")] Overflow, #[error("division by zero error while calculating protocol fee")] diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index f9fcab025d..8c6975bb59 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -189,11 +189,8 @@ impl Solution { sell: self.prices[&fulfillment.order().sell.token.wrap(self.weth)], buy: self.prices[&fulfillment.order().buy.token.wrap(self.weth)], }; - trades.push( - fulfillment - .with_protocol_fee(prices) - .map(Trade::Fulfillment)?, - ); + let fulfillment = fulfillment.with_protocol_fee(prices)?; + trades.push(Trade::Fulfillment(fulfillment)) } order::Kind::Liquidity => { trades.push(trade); @@ -350,5 +347,5 @@ pub enum SolutionError { #[error("invalid clearing prices")] InvalidClearingPrices, #[error(transparent)] - FailedProtocolFee(#[from] fee::Error), + ProtocolFee(#[from] fee::Error), } diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 3b0436379f..3f8d9c6d46 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -43,11 +43,11 @@ impl Fulfillment { }), }; + let executed_with_fee = + order::TargetAmount(executed.0.checked_add(fee.0).ok_or(InvalidExecutedAmount)?); match order.partial { - order::Partial::Yes { available } => { - order::TargetAmount(executed.0 + fee.0) <= available - } - order::Partial::No => order::TargetAmount(executed.0 + fee.0) == order.target(), + order::Partial::Yes { available } => executed_with_fee <= available, + order::Partial::No => executed_with_fee == order.target(), } }; @@ -95,10 +95,12 @@ impl Fulfillment { } } - /// Returns the raw internal representation of the fee that contains - /// original source of the fee - pub fn raw_fee(&self) -> Fee { - self.fee + /// Returns the solver determined fee if it exists. + pub fn dynamic_fee(&self) -> Option { + match self.fee { + Fee::Static => None, + Fee::Dynamic(fee) => Some(fee), + } } /// The effective amount that left the user's wallet including all fees. diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index 9692124c61..f51a9e7b79 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -210,7 +210,7 @@ impl Solutions { competition::solution::SolutionError::InvalidClearingPrices => { super::Error("invalid clearing prices") } - competition::solution::SolutionError::FailedProtocolFee(_) => { + competition::solution::SolutionError::ProtocolFee(_) => { super::Error("failed protocol fee") } }) diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 6f225d87cd..d00265f93c 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -108,10 +108,6 @@ pub fn solve_req(test: &Test) -> serde_json::Value { "kind": "priceimprovement", "factor": 0.5, "maxVolumeFactor": 0.06, - }), - json!({ - "kind": "volume", - "factor": 0.1, }) ], }, From 490f0b8b3149c26dcf36ec73d6ecbd2464ee2200 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 29 Dec 2023 11:59:17 +0100 Subject: [PATCH 21/35] sell amount expectation --- crates/driver/src/tests/setup/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 5a10f74f86..93f84e06d7 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -862,7 +862,7 @@ impl<'a> SolveOk<'a> { assert!(u256(trade.get("buyAmount").unwrap()) == expected.quoted_order.buy); assert!( u256(trade.get("sellAmount").unwrap()) - == expected.quoted_order.sell + expected.quoted_order.order.user_fee + >= expected.quoted_order.sell + expected.quoted_order.order.user_fee ); } self From b9df28f2d14ebf21784847eeb88573ad2e624d6c Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 29 Dec 2023 12:24:43 +0100 Subject: [PATCH 22/35] removed UTs --- .../src/domain/competition/solution/fee.rs | 359 ------------------ crates/driver/src/tests/setup/mod.rs | 2 +- 2 files changed, 1 insertion(+), 360 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index d5d6d36247..8a736e6e30 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -221,362 +221,3 @@ pub enum Error { #[error(transparent)] InvalidExecutedAmount(#[from] InvalidExecutedAmount), } - -mod tests { - use { - super::*, - crate::{ - domain::{ - competition, - competition::order::{ - signature::Scheme, - AppData, - BuyTokenBalance, - FeePolicy, - SellTokenBalance, - Signature, - TargetAmount, - }, - }, - util, - }, - primitive_types::{H160, U256}, - std::str::FromStr, - }; - - #[test] - fn test_fulfillment_sell_limit_order_fok() { - // https://explorer.cow.fi/orders/0xef6de27933bde867c768ead05d34a08c806d35b89f6bea565bdeb40108265e9a6f419390da10911abd1e1c962b569312a9c9c7b1658a2936?tab=overview - let order = competition::Order { - uid: Default::default(), - side: order::Side::Sell, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xba3335588d9403515223f109edc4eb7269a9ab5d").unwrap(), - )), - amount: eth::TokenAmount(778310860032541096349039u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), - )), - amount: eth::TokenAmount(4166666666666666666u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::No, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.25?id=m8dnoowB4Ql8nk7a5ber - let prices = ClearingPrices { - sell: eth::U256::from(913320970421237626580182u128), - buy: eth::U256::from(4149866666666666668u128), - }; - let executed = order::TargetAmount(4149866666666666668u128.into()); - let fee = Fee::Dynamic(order::SellAmount(16799999999999998u128.into())); - let fulfillment = Fulfillment::new(order.clone(), executed, fee).unwrap(); - // fee does not contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount(16799999999999998u128.into()) - ); - // executed amount before protocol fee - assert_eq!(fulfillment.executed(), executed); - - let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((16799999999999998u128 + 306723471216604081u128).into()) - ); - // executed amount reduced by protocol fee - assert_eq!( - fulfillment.executed(), - U256::from(3843143195450062587u128).into() - ); // 4149866666666666668 - 306723471216604081 - } - - #[test] - pub fn test_fulfillment_buy_limit_order_fok() { - // https://explorer.cow.fi/orders/0xc9096a3dbfb1f661e65ecc14644adec6bd8e385ae818aa73181def24996affb589e4042fd85e857e81a4fa89831b1f5ad4f384b7659357d7?tab=overview - let order = competition::Order { - uid: Default::default(), - side: order::Side::Buy, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), - )), - amount: eth::TokenAmount(170000000000000000u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab").unwrap(), - )), - amount: eth::TokenAmount(1781433576205823004786u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::No, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - // taken from https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=cYSDo4wBlutGF6Gybl6x - let prices = ClearingPrices { - sell: eth::U256::from(7213317128720734077u128), - buy: eth::U256::from(74745150907421124481191u128), - }; - let executed = order::TargetAmount(170000000000000000u128.into()); - let fee = Fee::Dynamic(order::SellAmount(19868323826701104280u128.into())); - let fulfillment = Fulfillment::new(order.clone(), executed, fee).unwrap(); - // fee does not contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount(19868323826701104280u128.into()) - ); - // executed amount before protocol fee - assert_eq!(fulfillment.executed(), executed); - - let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((19868323826701104280u128 + 3684441086061450u128).into()) - ); - // executed amount same as before - assert_eq!(fulfillment.executed(), executed); - } - - #[test] - fn test_fulfillment_sell_limit_order_partial() { - // https://explorer.cow.fi/orders/0x1a146dba48512326c647aae1ce511206b373b151e1b9ada9772c313e7d24ec2e0960da039bb8151cacfef620476e8baf34bd95656594209e?tab=overview - // 3 fullfillments - // - // 1. tx hash 0xbc95b97d09a62e6a68b15a8dfd4655a6e25d100ce0dd98a6a43e3b7eac9951cc - // - // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=W-uxp4wBlutGF6GyxkCq - let order1 = competition::Order { - uid: Default::default(), - side: order::Side::Sell, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), - )), - amount: eth::TokenAmount(136363636363636u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), - amount: eth::TokenAmount(9000000000u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::Yes { - available: TargetAmount(9000000000u128.into()), - }, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - let prices = ClearingPrices { - sell: eth::U256::from(452471455796126723289489746u128), - buy: eth::U256::from(29563373796548615411833u128), - }; - let executed = order::TargetAmount(1746031488u128.into()); - let fee = Fee::Dynamic(order::SellAmount(11566733u128.into())); - let fulfillment = Fulfillment::new(order1.clone(), executed, fee).unwrap(); - // fee does not contains protocol fee - assert_eq!(fulfillment.fee(), order::SellAmount(11566733u128.into())); - // executed amount before protocol fee - assert_eq!(fulfillment.executed(), executed); - - let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((11566733u128 + 3037322u128).into()) - ); - // executed amount reduced by protocol fee - assert_eq!(fulfillment.executed(), U256::from(1742994166u128).into()); // 1746031488 - 3037322 - - // 2. tx hash 0x2f9b928182649aad2eaf04361fff1aff3cb8d37e4988c952aed49465eff01c9e - // - // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=uvXcp4wB4Ql8nk7aQgeZ - - let order2 = competition::Order { - uid: Default::default(), - side: order::Side::Sell, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), - )), - amount: eth::TokenAmount(136363636363636u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), - amount: eth::TokenAmount(9000000000u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::Yes { - available: TargetAmount(7242401779u128.into()), - }, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - let prices = ClearingPrices { - sell: eth::U256::from(49331008874302634851980418220032u128), - buy: eth::U256::from(3204738565525085525012119552u128), - }; - let executed = order::TargetAmount(2887238741u128.into()); - let fee = Fee::Dynamic(order::SellAmount(27827963u128.into())); - let fulfillment = Fulfillment::new(order2.clone(), executed, fee).unwrap(); - // fee does not contains protocol fee - assert_eq!(fulfillment.fee(), order::SellAmount(27827963u128.into())); - // executed amount before protocol fee - assert_eq!(fulfillment.executed(), executed); - - let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((27827963u128 + 8965365u128).into()) - ); - // executed amount reduced by protocol fee - assert_eq!(fulfillment.executed(), U256::from(2878273376u128).into()); // 2887238741 - 8965365 - - // 3. 0x813dab5983fd3643e1ce3e7efbdbfe1ca8c41419bcfaf1e898e067e37c455d75 - // - // https://production-6de61f.kb.eu-central-1.aws.cloud.es.io/app/discover#/doc/c0e240e0-d9b3-11ed-b0e6-e361adffce0b/cowlogs-prod-2023.12.26?id=xPXdp4wB4Ql8nk7a8ert - - let order3 = competition::Order { - uid: Default::default(), - side: order::Side::Sell, - buy: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0x70edf1c215d0ce69e7f16fd4e6276ba0d99d4de7").unwrap(), - )), - amount: eth::TokenAmount(136363636363636u128.into()), - }, - sell: eth::Asset { - token: eth::TokenAddress(eth::ContractAddress( - H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), - amount: eth::TokenAmount(9000000000u128.into()), - }, - kind: order::Kind::Limit, - fee: Default::default(), - fee_policies: vec![FeePolicy::PriceImprovement { - factor: 0.5, - max_volume_factor: 1.0, - }], - partial: order::Partial::Yes { - available: TargetAmount(4327335075u128.into()), - }, - receiver: Default::default(), - pre_interactions: Default::default(), - post_interactions: Default::default(), - valid_to: util::Timestamp(0), - app_data: AppData(Default::default()), - sell_token_balance: SellTokenBalance::Erc20, - buy_token_balance: BuyTokenBalance::Erc20, - signature: Signature { - scheme: Scheme::Eip712, - data: Default::default(), - signer: eth::Address::default(), - }, - }; - - let prices = ClearingPrices { - sell: eth::U256::from(65841033847428u128), - buy: eth::U256::from(4302554937u128), - }; - let executed = order::TargetAmount(4302554937u128.into()); - let fee = Fee::Dynamic(order::SellAmount(24780138u128.into())); - let fulfillment = Fulfillment::new(order3.clone(), executed, fee).unwrap(); - // fee does not contains protocol fee - assert_eq!(fulfillment.fee(), order::SellAmount(24780138u128.into())); - // executed amount before protocol fee - assert_eq!(fulfillment.executed(), executed); - - let fulfillment = fulfillment.with_protocol_fee(prices).unwrap(); - // fee contains protocol fee - assert_eq!( - fulfillment.fee(), - order::SellAmount((24780138u128 + 8996762u128).into()) - ); - // executed amount reduced by protocol fee - assert_eq!(fulfillment.executed(), U256::from(4293558175u128).into()); // 4302554937 - 8996762 - } - - #[test] - fn test_checked_sub() { - assert_eq!(U256::from(1u128).checked_sub(U256::from(2u128)), None); - assert_eq!( - U256::from(2u128).checked_sub(U256::from(1u128)), - Some(U256::from(1u128)) - ); - } -} diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 93f84e06d7..d12440e2be 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -859,7 +859,7 @@ impl<'a> SolveOk<'a> { let u256 = |value: &serde_json::Value| { eth::U256::from_dec_str(value.as_str().unwrap()).unwrap() }; - assert!(u256(trade.get("buyAmount").unwrap()) == expected.quoted_order.buy); + assert!(u256(trade.get("buyAmount").unwrap()) <= expected.quoted_order.buy); assert!( u256(trade.get("sellAmount").unwrap()) >= expected.quoted_order.sell + expected.quoted_order.order.user_fee From 7b4939dffcf7643ea09ee4e52cb9711fdf7a39be Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 29 Dec 2023 12:59:14 +0100 Subject: [PATCH 23/35] revert driver tests --- crates/driver/src/tests/setup/driver.rs | 12 +----------- crates/driver/src/tests/setup/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index d00265f93c..509a8b5bdf 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -100,17 +100,7 @@ pub fn solve_req(test: &Test) -> serde_json::Value { "appData": "0x0000000000000000000000000000000000000000000000000000000000000000", "signingScheme": "eip712", "signature": format!("0x{}", hex::encode(quote.order_signature(&test.blockchain))), - "feePolicies": match quote.order.kind { - order::Kind::Market => vec![], - order::Kind::Liquidity => vec![], - order::Kind::Limit { .. } => vec![ - json!({ - "kind": "priceimprovement", - "factor": 0.5, - "maxVolumeFactor": 0.06, - }) - ], - }, + "feePolicies": [], })); } for fulfillment in test.fulfillments.iter() { diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index d12440e2be..5a10f74f86 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -859,10 +859,10 @@ impl<'a> SolveOk<'a> { let u256 = |value: &serde_json::Value| { eth::U256::from_dec_str(value.as_str().unwrap()).unwrap() }; - assert!(u256(trade.get("buyAmount").unwrap()) <= expected.quoted_order.buy); + assert!(u256(trade.get("buyAmount").unwrap()) == expected.quoted_order.buy); assert!( u256(trade.get("sellAmount").unwrap()) - >= expected.quoted_order.sell + expected.quoted_order.order.user_fee + == expected.quoted_order.sell + expected.quoted_order.order.user_fee ); } self From 490ecb4c934854c1c63e92f8007526d68395e155 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Sat, 6 Jan 2024 11:23:40 +0100 Subject: [PATCH 24/35] Add driver tests --- crates/driver/src/tests/cases/mod.rs | 1 + .../driver/src/tests/cases/protocol_fees.rs | 43 +++++++++++++++++ crates/driver/src/tests/setup/blockchain.rs | 39 +++++++++++++++- crates/driver/src/tests/setup/driver.rs | 5 +- crates/driver/src/tests/setup/mod.rs | 46 +++++++++++++++++-- 5 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 crates/driver/src/tests/cases/protocol_fees.rs diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index e6e6e0c4ff..450220ddd9 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -8,6 +8,7 @@ pub mod merge_settlements; pub mod multiple_solutions; pub mod negative_scores; pub mod order_prioritization; +pub mod protocol_fees; pub mod quote; pub mod score_competition; pub mod settle; diff --git a/crates/driver/src/tests/cases/protocol_fees.rs b/crates/driver/src/tests/cases/protocol_fees.rs new file mode 100644 index 0000000000..8bea5ab40e --- /dev/null +++ b/crates/driver/src/tests/cases/protocol_fees.rs @@ -0,0 +1,43 @@ +use crate::{ + domain::competition::order, + tests::{ + self, + setup::{ab_order, ab_pool, ab_solution, FeePolicy}, + }, +}; + +#[tokio::test] +#[ignore] +async fn protocol_fee() { + for side in [order::Side::Buy, order::Side::Sell] { + for fee_policy in [ + FeePolicy::PriceImprovement { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 1.0, + }, + FeePolicy::PriceImprovement { + factor: 0.5, + // low enough so we get capped by volume fee + max_volume_factor: 0.1, + }, + ] { + let test = tests::setup() + .name(format!("Protocol Fee: {side:?} {fee_policy:?}")) + .pool(ab_pool()) + .order( + ab_order() + .kind(order::Kind::Limit) + .side(side) + .solver_fee(Some(10000000000000000000u128.into())) + .set_surplus(2.into()) + .fee_policy(fee_policy), + ) + .solution(ab_solution()) + .done() + .await; + + test.solve().await.ok().orders(&[ab_order().name]); + } + } +} diff --git a/crates/driver/src/tests/setup/blockchain.rs b/crates/driver/src/tests/setup/blockchain.rs index 266c4df929..5bc81cfc79 100644 --- a/crates/driver/src/tests/setup/blockchain.rs +++ b/crates/driver/src/tests/setup/blockchain.rs @@ -1,5 +1,5 @@ use { - super::{Asset, Order, Partial, Score}, + super::{Asset, FeePolicy, Order, Partial, Score}, crate::{ domain::{ competition::order, @@ -136,6 +136,43 @@ impl QuotedOrder { partially_fillable: matches!(self.order.partial, Partial::Yes { .. }), } } + + /// Returns the surplus denominated in surplus token. + pub fn surplus(&self) -> eth::U256 { + match self.order.side { + order::Side::Buy => self.sell_amount() - self.sell, + order::Side::Sell => self.buy - self.buy_amount(), + } + } + + /// Returns the protocol fee denominated in surplus token. + pub fn protocol_fee(&self) -> eth::U256 { + self.order + .fee_policy + .as_ref() + .map(|policy| match policy { + FeePolicy::PriceImprovement { + factor, + max_volume_factor, + } => { + let price_improvement_fee = + self.surplus() * eth::U256::from_f64_lossy(factor * 100.) / 100; + let max_volume_fee = match self.order.side { + order::Side::Buy => { + self.sell * eth::U256::from_f64_lossy(max_volume_factor * 100.) / 100 + } + order::Side::Sell => { + self.buy * self.sell / (self.sell - self.order.surplus_fee()) + * eth::U256::from_f64_lossy(max_volume_factor * 100.) + / 100 + } + }; + std::cmp::min(price_improvement_fee, max_volume_fee) + } + FeePolicy::Volume { factor: _factor } => unimplemented!(), + }) + .unwrap_or_default() + } } pub struct Config { diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 509a8b5bdf..74ffbd9a57 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -100,7 +100,10 @@ pub fn solve_req(test: &Test) -> serde_json::Value { "appData": "0x0000000000000000000000000000000000000000000000000000000000000000", "signingScheme": "eip712", "signature": format!("0x{}", hex::encode(quote.order_signature(&test.blockchain))), - "feePolicies": [], + "feePolicies": match "e.order.fee_policy { + None => json!([]), + Some(policy) => json!([policy]), + }, })); } for fulfillment in test.fulfillments.iter() { diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 5a10f74f86..a5cea9bb4a 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -74,7 +74,18 @@ impl Default for Score { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[serde_as] +#[derive(Debug, Clone, PartialEq, serde::Serialize)] +#[serde(rename_all = "lowercase", tag = "kind")] +pub enum FeePolicy { + #[serde(rename_all = "camelCase")] + PriceImprovement { factor: f64, max_volume_factor: f64 }, + #[serde(rename_all = "camelCase")] + #[allow(dead_code)] + Volume { factor: f64 }, +} + +#[derive(Debug, Clone, PartialEq)] pub struct Order { pub name: &'static str, @@ -106,6 +117,8 @@ pub struct Order { /// Should the trader account be funded with enough tokens to place this /// order? True by default. pub funded: bool, + /// Should protocol fees be applied to this order? + pub fee_policy: Option, } impl Order { @@ -146,6 +159,14 @@ impl Order { } } + /// Set the surplus factor. + pub fn set_surplus(self, surplus_factor: eth::U256) -> Self { + Self { + surplus_factor, + ..self + } + } + /// Mark this order as internalizable. pub fn internalize(self) -> Self { Self { @@ -201,6 +222,14 @@ impl Order { _ => 0.into(), } } + + /// Set fee policy + pub fn fee_policy(self, fee_policy: FeePolicy) -> Self { + Self { + fee_policy: Some(fee_policy), + ..self + } + } } impl Default for Order { @@ -221,6 +250,7 @@ impl Default for Order { executed: Default::default(), filtered: Default::default(), funded: true, + fee_policy: Default::default(), } } } @@ -859,10 +889,20 @@ impl<'a> SolveOk<'a> { let u256 = |value: &serde_json::Value| { eth::U256::from_dec_str(value.as_str().unwrap()).unwrap() }; - assert!(u256(trade.get("buyAmount").unwrap()) == expected.quoted_order.buy); + + let (protocol_fee_sell, protocol_fee_buy) = match expected.quoted_order.order.side { + order::Side::Buy => (expected.quoted_order.protocol_fee(), 0.into()), + order::Side::Sell => (0.into(), expected.quoted_order.protocol_fee()), + }; + assert!( + u256(trade.get("buyAmount").unwrap()) + == (expected.quoted_order.buy - protocol_fee_buy) + ); assert!( u256(trade.get("sellAmount").unwrap()) - == expected.quoted_order.sell + expected.quoted_order.order.user_fee + == expected.quoted_order.sell + + expected.quoted_order.order.user_fee + + protocol_fee_sell ); } self From e2bc18cbba21759d52f0dbe619be13615feb2a44 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Sat, 6 Jan 2024 11:31:51 +0100 Subject: [PATCH 25/35] cr fixes --- crates/driver/src/domain/competition/solution/fee.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 8a736e6e30..d625feb743 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -46,7 +46,9 @@ impl Fulfillment { } Fee::Static } - Some(fee) => Fee::Dynamic((fee.0 + protocol_fee).into()), + Some(fee) => { + Fee::Dynamic((fee.0.checked_sub(protocol_fee).ok_or(Error::Overflow)?).into()) + } }; // Reduce the executed amount by the protocol fee. This is because solvers are @@ -126,6 +128,8 @@ impl Fulfillment { .checked_div(buy_amount) .ok_or(Error::DivisionByZero)?; // Remaining surplus after fees + // Do not return error if `checked_sub` fails because violated limit prices will + // be caught by simulation let surplus = limit_sell_amount .checked_sub(executed_sell_amount_with_fee) .unwrap_or(eth::U256::zero()); @@ -150,6 +154,8 @@ impl Fulfillment { .checked_div(sell_amount) .ok_or(Error::DivisionByZero)?; // Remaining surplus after fees + // Do not return error if `checked_sub` fails because violated limit prices will + // be caught by simulation let surplus = executed_buy_amount .checked_sub(limit_buy_amount) .unwrap_or(eth::U256::zero()); From 376785325a34135227194f03f09cfa9d14b2c018 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Sat, 6 Jan 2024 11:39:09 +0100 Subject: [PATCH 26/35] removed loop --- .../driver/src/domain/competition/solution/fee.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index d625feb743..4487e5477e 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -75,22 +75,19 @@ impl Fulfillment { } let mut protocol_fee = eth::U256::zero(); - for fee_policy in self.order().fee_policies.clone() { + if let Some(fee_policy) = self.order().fee_policies.first() { match fee_policy { FeePolicy::PriceImprovement { factor, max_volume_factor, } => { - let price_improvement_fee = self.price_improvement_fee(prices, factor)?; - let max_volume_fee = self.volume_fee(prices, max_volume_factor)?; + let price_improvement_fee = self.price_improvement_fee(prices, *factor)?; + let max_volume_fee = self.volume_fee(prices, *max_volume_factor)?; // take the smaller of the two - protocol_fee = protocol_fee - .checked_add(std::cmp::min(price_improvement_fee, max_volume_fee)) - .ok_or(Error::Overflow)?; + protocol_fee = std::cmp::min(price_improvement_fee, max_volume_fee); } FeePolicy::Volume { factor } => { - let fee = self.volume_fee(prices, factor)?; - protocol_fee = protocol_fee.checked_add(fee).ok_or(Error::Overflow)?; + protocol_fee = self.volume_fee(prices, *factor)?; } } } From 0cb7c19b65b23ef9b809e13f6c02d3ce0d744c51 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Sat, 6 Jan 2024 11:41:36 +0100 Subject: [PATCH 27/35] camelcase --- crates/driver/src/tests/setup/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 8ebdeb7ccb..19767ad537 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -81,7 +81,7 @@ impl Default for Score { #[serde_as] #[derive(Debug, Clone, PartialEq, serde::Serialize)] -#[serde(rename_all = "lowercase", tag = "kind")] +#[serde(rename_all = "camelCase", tag = "kind")] pub enum FeePolicy { #[serde(rename_all = "camelCase")] PriceImprovement { factor: f64, max_volume_factor: f64 }, From 32c100ecdd51316d702ba2226cf0ee8842fe260d Mon Sep 17 00:00:00 2001 From: sunce86 Date: Sat, 6 Jan 2024 11:47:01 +0100 Subject: [PATCH 28/35] facepalm --- crates/driver/src/domain/competition/solution/fee.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 4487e5477e..fdf03d28ba 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -47,7 +47,7 @@ impl Fulfillment { Fee::Static } Some(fee) => { - Fee::Dynamic((fee.0.checked_sub(protocol_fee).ok_or(Error::Overflow)?).into()) + Fee::Dynamic((fee.0.checked_add(protocol_fee).ok_or(Error::Overflow)?).into()) } }; From c10e82ca94bf4b02aff026963aa6d84bb321ea44 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 10 Jan 2024 14:19:58 +0100 Subject: [PATCH 29/35] reverted tests for protocol fee --- crates/driver/src/tests/cases/mod.rs | 1 - .../driver/src/tests/cases/protocol_fees.rs | 43 ------------------ crates/driver/src/tests/setup/blockchain.rs | 39 +--------------- crates/driver/src/tests/setup/mod.rs | 44 +------------------ 4 files changed, 3 insertions(+), 124 deletions(-) delete mode 100644 crates/driver/src/tests/cases/protocol_fees.rs diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index faf250da6e..4a3cf67d64 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -9,7 +9,6 @@ pub mod multiple_drivers; pub mod multiple_solutions; pub mod negative_scores; pub mod order_prioritization; -pub mod protocol_fees; pub mod quote; pub mod score_competition; pub mod settle; diff --git a/crates/driver/src/tests/cases/protocol_fees.rs b/crates/driver/src/tests/cases/protocol_fees.rs deleted file mode 100644 index 8bea5ab40e..0000000000 --- a/crates/driver/src/tests/cases/protocol_fees.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::{ - domain::competition::order, - tests::{ - self, - setup::{ab_order, ab_pool, ab_solution, FeePolicy}, - }, -}; - -#[tokio::test] -#[ignore] -async fn protocol_fee() { - for side in [order::Side::Buy, order::Side::Sell] { - for fee_policy in [ - FeePolicy::PriceImprovement { - factor: 0.5, - // high enough so we don't get capped by volume fee - max_volume_factor: 1.0, - }, - FeePolicy::PriceImprovement { - factor: 0.5, - // low enough so we get capped by volume fee - max_volume_factor: 0.1, - }, - ] { - let test = tests::setup() - .name(format!("Protocol Fee: {side:?} {fee_policy:?}")) - .pool(ab_pool()) - .order( - ab_order() - .kind(order::Kind::Limit) - .side(side) - .solver_fee(Some(10000000000000000000u128.into())) - .set_surplus(2.into()) - .fee_policy(fee_policy), - ) - .solution(ab_solution()) - .done() - .await; - - test.solve().await.ok().orders(&[ab_order().name]); - } - } -} diff --git a/crates/driver/src/tests/setup/blockchain.rs b/crates/driver/src/tests/setup/blockchain.rs index cd19742606..2b01d797c0 100644 --- a/crates/driver/src/tests/setup/blockchain.rs +++ b/crates/driver/src/tests/setup/blockchain.rs @@ -1,5 +1,5 @@ use { - super::{Asset, FeePolicy, Order, Partial, Score}, + super::{Asset, Order, Partial, Score}, crate::{ domain::{ competition::order, @@ -134,43 +134,6 @@ impl QuotedOrder { partially_fillable: matches!(self.order.partial, Partial::Yes { .. }), } } - - /// Returns the surplus denominated in surplus token. - pub fn surplus(&self) -> eth::U256 { - match self.order.side { - order::Side::Buy => self.sell_amount() - self.sell, - order::Side::Sell => self.buy - self.buy_amount(), - } - } - - /// Returns the protocol fee denominated in surplus token. - pub fn protocol_fee(&self) -> eth::U256 { - self.order - .fee_policy - .as_ref() - .map(|policy| match policy { - FeePolicy::PriceImprovement { - factor, - max_volume_factor, - } => { - let price_improvement_fee = - self.surplus() * eth::U256::from_f64_lossy(factor * 100.) / 100; - let max_volume_fee = match self.order.side { - order::Side::Buy => { - self.sell * eth::U256::from_f64_lossy(max_volume_factor * 100.) / 100 - } - order::Side::Sell => { - self.buy * self.sell / (self.sell - self.order.surplus_fee()) - * eth::U256::from_f64_lossy(max_volume_factor * 100.) - / 100 - } - }; - std::cmp::min(price_improvement_fee, max_volume_fee) - } - FeePolicy::Volume { factor: _factor } => unimplemented!(), - }) - .unwrap_or_default() - } } pub struct Config { diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 19767ad537..e13aa3740d 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -79,17 +79,6 @@ impl Default for Score { } } -#[serde_as] -#[derive(Debug, Clone, PartialEq, serde::Serialize)] -#[serde(rename_all = "camelCase", tag = "kind")] -pub enum FeePolicy { - #[serde(rename_all = "camelCase")] - PriceImprovement { factor: f64, max_volume_factor: f64 }, - #[serde(rename_all = "camelCase")] - #[allow(dead_code)] - Volume { factor: f64 }, -} - #[derive(Debug, Clone, PartialEq)] pub struct Order { pub name: &'static str, @@ -122,8 +111,6 @@ pub struct Order { /// Should the trader account be funded with enough tokens to place this /// order? True by default. pub funded: bool, - /// Should protocol fees be applied to this order? - pub fee_policy: Option, } impl Order { @@ -164,14 +151,6 @@ impl Order { } } - /// Set the surplus factor. - pub fn set_surplus(self, surplus_factor: eth::U256) -> Self { - Self { - surplus_factor, - ..self - } - } - /// Mark this order as internalizable. pub fn internalize(self) -> Self { Self { @@ -227,14 +206,6 @@ impl Order { _ => 0.into(), } } - - /// Set fee policy - pub fn fee_policy(self, fee_policy: FeePolicy) -> Self { - Self { - fee_policy: Some(fee_policy), - ..self - } - } } impl Default for Order { @@ -255,7 +226,6 @@ impl Default for Order { executed: Default::default(), filtered: Default::default(), funded: true, - fee_policy: Default::default(), } } } @@ -945,20 +915,10 @@ impl<'a> SolveOk<'a> { let u256 = |value: &serde_json::Value| { eth::U256::from_dec_str(value.as_str().unwrap()).unwrap() }; - - let (protocol_fee_sell, protocol_fee_buy) = match expected.quoted_order.order.side { - order::Side::Buy => (expected.quoted_order.protocol_fee(), 0.into()), - order::Side::Sell => (0.into(), expected.quoted_order.protocol_fee()), - }; - assert!( - u256(trade.get("buyAmount").unwrap()) - == (expected.quoted_order.buy - protocol_fee_buy) - ); + assert!(u256(trade.get("buyAmount").unwrap()) == expected.quoted_order.buy); assert!( u256(trade.get("sellAmount").unwrap()) - == expected.quoted_order.sell - + expected.quoted_order.order.user_fee - + protocol_fee_sell + == expected.quoted_order.sell + expected.quoted_order.order.user_fee ); } self From 010cfc4105595856a46bd5b0b28ffea36f36fb2d Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 10 Jan 2024 14:24:25 +0100 Subject: [PATCH 30/35] fix failing tests --- crates/driver/src/tests/setup/driver.rs | 2 +- crates/driver/src/tests/setup/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 75c26e0d12..97bfbd0b65 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -96,7 +96,7 @@ pub fn solve_req(test: &Test) -> serde_json::Value { "signature": format!("0x{}", hex::encode(quote.order_signature(&test.blockchain))), "feePolicies": [{ "priceImprovement": { - "factor": 0.5, + "factor": 0.0, "maxVolumeFactor": 0.06 } }], diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index e13aa3740d..a7d2f6f0e2 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -287,7 +287,7 @@ impl Solver { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct Pool { pub token_a: &'static str, pub token_b: &'static str, From 42cd81d9c2d5498dbd5aeec6edaacd205b7308aa Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 10 Jan 2024 14:41:52 +0100 Subject: [PATCH 31/35] fix driver tests --- crates/driver/src/tests/setup/driver.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 97bfbd0b65..f7c3bfdd65 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -94,12 +94,16 @@ pub fn solve_req(test: &Test) -> serde_json::Value { "appData": "0x0000000000000000000000000000000000000000000000000000000000000000", "signingScheme": "eip712", "signature": format!("0x{}", hex::encode(quote.order_signature(&test.blockchain))), - "feePolicies": [{ - "priceImprovement": { - "factor": 0.0, - "maxVolumeFactor": 0.06 - } - }], + "feePolicies": match quote.order.kind { + order::Kind::Market => json!([]), + order::Kind::Liquidity => json!([]), + order::Kind::Limit { .. } => json!([{ + "priceImprovement": { + "factor": 0.0, + "maxVolumeFactor": 0.06 + } + }]), + }, })); } for fulfillment in test.fulfillments.iter() { From 3fa36b9c2a66c37025530067e2b6caea70670b9a Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 10 Jan 2024 14:52:55 +0100 Subject: [PATCH 32/35] fix comments --- crates/driver/src/domain/competition/solution/fee.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index fdf03d28ba..7adb9eb600 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -6,8 +6,8 @@ //! //! SELL ORDER //! Selling 1 WETH for at least `x` amount of USDC. Solvers respond with -//! Fee = 0.05 WETH -//! Executed = 0.95 WETH +//! Fee = 0.05 WETH (always expressed in sell token) +//! Executed = 0.95 WETH (always expressed in target token) //! //! This response is adjusted by the protocol fee of 0.1 WETH: //! Fee = 0.05 WETH + 0.1 WETH = 0.15 WETH @@ -15,11 +15,11 @@ //! //! BUY ORDER //! Buying 1 WETH for at most `x` amount of USDC. Solvers respond with -//! Fee = 0.05 WETH -//! Executed = 1 WETH +//! Fee = 10 USDC (always expressed in sell token) +//! Executed = 1 WETH (always expressed in target token) //! -//! This response is adjusted by the protocol fee of 0.1 WETH: -//! Fee = 0.05 WETH + 0.1 WETH = 0.15 WETH +//! This response is adjusted by the protocol fee of 5 USDC: +//! Fee = 10 USDC + 5 USDC = 15 USDC //! Executed = 1 WETH use { From 0f5d3742cf3b49f9d3db561117930e681328bc1c Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 11 Jan 2024 11:38:02 +0100 Subject: [PATCH 33/35] cr fixes --- .../src/domain/competition/solution/fee.rs | 137 ++++++++---------- .../src/domain/competition/solution/mod.rs | 2 +- .../src/domain/competition/solution/trade.rs | 2 +- crates/driver/src/infra/solver/dto/mod.rs | 2 +- .../driver/src/infra/solver/dto/solution.rs | 16 +- 5 files changed, 76 insertions(+), 83 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 7adb9eb600..d19168eb0d 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -39,7 +39,7 @@ impl Fulfillment { let protocol_fee = self.protocol_fee(prices)?; // Increase the fee by the protocol fee - let fee = match self.dynamic_fee() { + let fee = match self.surplus_fee() { None => { if !protocol_fee.is_zero() { return Err(Error::ProtocolFeeOnStaticOrder); @@ -74,24 +74,19 @@ impl Fulfillment { return Err(Error::MultipleFeePolicies); } - let mut protocol_fee = eth::U256::zero(); - if let Some(fee_policy) = self.order().fee_policies.first() { - match fee_policy { - FeePolicy::PriceImprovement { - factor, - max_volume_factor, - } => { - let price_improvement_fee = self.price_improvement_fee(prices, *factor)?; - let max_volume_fee = self.volume_fee(prices, *max_volume_factor)?; - // take the smaller of the two - protocol_fee = std::cmp::min(price_improvement_fee, max_volume_fee); - } - FeePolicy::Volume { factor } => { - protocol_fee = self.volume_fee(prices, *factor)?; - } + match self.order().fee_policies.first() { + Some(FeePolicy::PriceImprovement { + factor, + max_volume_factor, + }) => { + let price_improvement_fee = self.price_improvement_fee(prices, *factor)?; + let max_volume_fee = self.volume_fee(prices, *max_volume_factor)?; + // take the smaller of the two + Ok(std::cmp::min(price_improvement_fee, max_volume_fee)) } + Some(FeePolicy::Volume { factor }) => self.volume_fee(prices, *factor), + None => Ok(0.into()), } - Ok(protocol_fee) } fn price_improvement_fee( @@ -102,22 +97,28 @@ impl Fulfillment { let sell_amount = self.order().sell.amount.0; let buy_amount = self.order().buy.amount.0; let executed = self.executed().0; - let surplus_fee = self - .dynamic_fee() - .map(|fee| fee.0) - .ok_or(Error::ProtocolFeeOnStaticOrder)?; - match self.order().side { + let executed_sell_amount = match self.order().side { Side::Buy => { // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` - let executed_sell_amount = executed + executed .checked_mul(prices.buy) .ok_or(Error::Overflow)? .checked_div(prices.sell) - .ok_or(Error::DivisionByZero)?; - // Sell slightly more `sell_token` to capture the `surplus_fee` - let executed_sell_amount_with_fee = executed_sell_amount - .checked_add(surplus_fee) - .ok_or(Error::Overflow)?; + .ok_or(Error::DivisionByZero)? + } + Side::Sell => executed, + }; + // Sell slightly more `sell_token` to capture the `surplus_fee` + let executed_sell_amount_with_fee = executed_sell_amount + .checked_add( + // surplus_fee is always expressed in sell token + self.surplus_fee() + .map(|fee| fee.0) + .ok_or(Error::ProtocolFeeOnStaticOrder)?, + ) + .ok_or(Error::Overflow)?; + let surplus_in_sell_token = match self.order().side { + Side::Buy => { // Scale to support partially fillable orders let limit_sell_amount = sell_amount .checked_mul(executed) @@ -127,83 +128,73 @@ impl Fulfillment { // Remaining surplus after fees // Do not return error if `checked_sub` fails because violated limit prices will // be caught by simulation - let surplus = limit_sell_amount + limit_sell_amount .checked_sub(executed_sell_amount_with_fee) - .unwrap_or(eth::U256::zero()); - Ok(surplus - .checked_mul(eth::U256::from_f64_lossy(factor * 100.)) - .ok_or(Error::Overflow)? - / 100) + .unwrap_or(eth::U256::zero()) } Side::Sell => { - // How much `buy_token` we get for `executed` amount of `sell_token` - let executed_buy_amount = executed - .checked_mul(prices.sell) - .ok_or(Error::Overflow)? - .checked_div(prices.buy) - .ok_or(Error::DivisionByZero)?; - let executed_sell_amount_with_fee = - executed.checked_add(surplus_fee).ok_or(Error::Overflow)?; // Scale to support partially fillable orders let limit_buy_amount = buy_amount .checked_mul(executed_sell_amount_with_fee) .ok_or(Error::Overflow)? .checked_div(sell_amount) .ok_or(Error::DivisionByZero)?; + // How much `buy_token` we get for `executed` amount of `sell_token` + let executed_buy_amount = executed + .checked_mul(prices.sell) + .ok_or(Error::Overflow)? + .checked_div(prices.buy) + .ok_or(Error::DivisionByZero)?; // Remaining surplus after fees // Do not return error if `checked_sub` fails because violated limit prices will // be caught by simulation let surplus = executed_buy_amount .checked_sub(limit_buy_amount) .unwrap_or(eth::U256::zero()); - let surplus_in_sell_token = surplus + // surplus in sell token + surplus .checked_mul(prices.buy) .ok_or(Error::Overflow)? .checked_div(prices.sell) - .ok_or(Error::DivisionByZero)?; - Ok(surplus_in_sell_token - .checked_mul(eth::U256::from_f64_lossy(factor * 100.)) - .ok_or(Error::Overflow)? - / 100) + .ok_or(Error::DivisionByZero)? } - } + }; + apply_factor(surplus_in_sell_token, factor) } fn volume_fee(&self, prices: ClearingPrices, factor: f64) -> Result { let executed = self.executed().0; - let surplus_fee = self - .dynamic_fee() - .map(|fee| fee.0) - .ok_or(Error::ProtocolFeeOnStaticOrder)?; - match self.order().side { + let executed_sell_amount = match self.order().side { Side::Buy => { // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` - let executed_sell_amount = executed + executed .checked_mul(prices.buy) .ok_or(Error::Overflow)? .checked_div(prices.sell) - .ok_or(Error::DivisionByZero)?; - // Sell slightly more `sell_token` to capture the `surplus_fee` - let executed_sell_amount_with_fee = executed_sell_amount - .checked_add(surplus_fee) - .ok_or(Error::Overflow)?; - Ok(executed_sell_amount_with_fee - .checked_mul(eth::U256::from_f64_lossy(factor * 100.)) - .ok_or(Error::Overflow)? - / 100) - } - Side::Sell => { - let executed_sell_amount_with_fee = - executed.checked_add(surplus_fee).ok_or(Error::Overflow)?; - Ok(executed_sell_amount_with_fee - .checked_mul(eth::U256::from_f64_lossy(factor * 100.)) - .ok_or(Error::Overflow)? - / 100) + .ok_or(Error::DivisionByZero)? } - } + Side::Sell => executed, + }; + // Sell slightly more `sell_token` to capture the `surplus_fee` + let executed_sell_amount_with_fee = executed_sell_amount + .checked_add( + // surplus_fee is always expressed in sell token + self.surplus_fee() + .map(|fee| fee.0) + .ok_or(Error::ProtocolFeeOnStaticOrder)?, + ) + .ok_or(Error::Overflow)?; + apply_factor(executed_sell_amount_with_fee, factor) } } +fn apply_factor(amount: eth::U256, factor: f64) -> Result { + Ok(amount + .checked_mul(eth::U256::from_f64_lossy(factor * 10000.)) + .ok_or(Error::Overflow)? + / 10000) +} + /// Uniform clearing prices at which the trade was executed. #[derive(Debug, Clone, Copy)] pub struct ClearingPrices { diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index a966ec519d..8b8b33273c 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -179,7 +179,7 @@ impl Solution { } pub fn with_protocol_fees(self) -> Result { - let mut trades = Vec::new(); + let mut trades = Vec::with_capacity(self.trades.len()); for trade in self.trades { match &trade { Trade::Fulfillment(fulfillment) => match fulfillment.order().kind { diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 3f8d9c6d46..6fd642430a 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -96,7 +96,7 @@ impl Fulfillment { } /// Returns the solver determined fee if it exists. - pub fn dynamic_fee(&self) -> Option { + pub fn surplus_fee(&self) -> Option { match self.fee { Fee::Static => None, Fee::Dynamic(fee) => Some(fee), diff --git a/crates/driver/src/infra/solver/dto/mod.rs b/crates/driver/src/infra/solver/dto/mod.rs index 0adb77f95e..117d5c16fe 100644 --- a/crates/driver/src/infra/solver/dto/mod.rs +++ b/crates/driver/src/infra/solver/dto/mod.rs @@ -8,4 +8,4 @@ pub use {auction::Auction, notification::Notification, solution::Solutions}; #[derive(Debug, thiserror::Error)] #[error("{0}")] -pub struct Error(pub &'static str); +pub struct Error(pub String); diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index bc35cfebc5..9143af46ed 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -34,7 +34,7 @@ impl Solutions { .find(|order| order.uid == fulfillment.order) // TODO this error should reference the UID .ok_or(super::Error( - "invalid order UID specified in fulfillment" + "invalid order UID specified in fulfillment".to_owned() ))? .clone(); @@ -51,7 +51,7 @@ impl Solutions { .map(competition::solution::Trade::Fulfillment) .map_err( |competition::solution::trade::InvalidExecutedAmount| { - super::Error("invalid trade fulfillment") + super::Error("invalid trade fulfillment".to_owned()) }, ) } @@ -117,7 +117,9 @@ impl Solutions { ) .map_err( |competition::solution::trade::InvalidExecutedAmount| { - super::Error("invalid executed amount in JIT order") + super::Error( + "invalid executed amount in JIT order".to_owned(), + ) }, )?, )), @@ -175,7 +177,7 @@ impl Solutions { .iter() .find(|liquidity| liquidity.id == interaction.id) .ok_or(super::Error( - "invalid liquidity ID specified in interaction", + "invalid liquidity ID specified in interaction".to_owned(), ))? .to_owned(); Ok(competition::solution::Interaction::Liquidity( @@ -208,10 +210,10 @@ impl Solutions { ) .map_err(|err| match err { competition::solution::SolutionError::InvalidClearingPrices => { - super::Error("invalid clearing prices") + super::Error("invalid clearing prices".to_owned()) } - competition::solution::SolutionError::ProtocolFee(_) => { - super::Error("failed protocol fee") + competition::solution::SolutionError::ProtocolFee(err) => { + super::Error(format!("could not incorporate protocol fee: {err}")) } }) }) From 59d8dae4bea888c05386c504c7748357b122dea3 Mon Sep 17 00:00:00 2001 From: Dusan Stanivukovic Date: Thu, 11 Jan 2024 20:01:03 +0100 Subject: [PATCH 34/35] E2E test protocol fee (#2260) # Description Merges to https://github.com/cowprotocol/services/pull/2213, not main. Adds e2e tests for protocol fee calculation. - Sell order - Sell order, but protocol fee capped by max volume - Buy order - Buy order, but protocol fee capped by max volume Asserts that if `executed_surplus_fee = X` without protocol fees, then `executed_surplus_fee = X + protocol_fee` with protocol fees. --- crates/e2e/tests/e2e/main.rs | 1 + crates/e2e/tests/e2e/protocol_fee.rs | 379 +++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 crates/e2e/tests/e2e/protocol_fee.rs diff --git a/crates/e2e/tests/e2e/main.rs b/crates/e2e/tests/e2e/main.rs index 560082fd2d..ea77b8ec80 100644 --- a/crates/e2e/tests/e2e/main.rs +++ b/crates/e2e/tests/e2e/main.rs @@ -18,6 +18,7 @@ mod order_cancellation; mod partial_fill; mod partially_fillable_balance; mod partially_fillable_pool; +mod protocol_fee; mod quoting; mod refunder; mod smart_contract_orders; diff --git a/crates/e2e/tests/e2e/protocol_fee.rs b/crates/e2e/tests/e2e/protocol_fee.rs new file mode 100644 index 0000000000..8c4045bb7f --- /dev/null +++ b/crates/e2e/tests/e2e/protocol_fee.rs @@ -0,0 +1,379 @@ +use { + e2e::{ + setup::{colocation::SolverEngine, *}, + tx, + }, + ethcontract::prelude::U256, + model::{ + order::{OrderCreation, OrderKind}, + signature::EcdsaSigningScheme, + }, + secp256k1::SecretKey, + shared::ethrpc::Web3, + web3::signing::SecretKeyRef, +}; + +#[tokio::test] +#[ignore] +async fn local_node_price_improvement_fee_sell_order() { + run_test(price_improvement_fee_sell_order_test).await; +} + +#[tokio::test] +#[ignore] +async fn local_node_price_improvement_fee_sell_order_capped() { + run_test(price_improvement_fee_sell_order_capped_test).await; +} + +#[tokio::test] +#[ignore] +async fn local_node_volume_fee_sell_order() { + run_test(volume_fee_sell_order_test).await; +} + +#[tokio::test] +#[ignore] +async fn local_node_price_improvement_fee_buy_order() { + run_test(price_improvement_fee_buy_order_test).await; +} + +#[tokio::test] +#[ignore] +async fn local_node_price_improvement_fee_buy_order_capped() { + run_test(price_improvement_fee_buy_order_capped_test).await; +} + +#[tokio::test] +#[ignore] +async fn local_node_volume_fee_buy_order() { + run_test(volume_fee_buy_order_test).await; +} + +async fn price_improvement_fee_sell_order_test(web3: Web3) { + let fee_policy = FeePolicyKind::PriceImprovement { + factor: 0.3, + max_volume_factor: 1.0, + }; + // Without protocol fee: + // Expected execution is 10000000000000000000 GNO for + // 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO + // + // With protocol fee: + // surplus [DAI] = 9871415430342266811 DAI - 5000000000000000000 DAI = + // 4871415430342266811 DAI + // + // protocol fee = 0.3*surplus = 1461424629102680043 DAI = + // 1461424629102680043 DAI / 9871415430342266811 * + // (10000000000000000000 - 167058994203399) = 1480436341679873337 GNO + // + // final execution is 10000000000000000000 GNO for 8409990801239586768 DAI, with + // executed_surplus_fee = 1480603400674076736 GNO + // + // Settlement contract balance after execution = 1480603400674076736 GNO = + // 1480603400674076736 GNO * 8409990801239586768 / (10000000000000000000 - + // 1480603400674076736) = 1461589542731026166 DAI + execute_test( + web3.clone(), + fee_policy, + OrderKind::Sell, + 1480603400674076736u128.into(), + 1461589542731026166u128.into(), + ) + .await; +} + +async fn price_improvement_fee_sell_order_capped_test(web3: Web3) { + let fee_policy = FeePolicyKind::PriceImprovement { + factor: 1.0, + max_volume_factor: 0.1, + }; + // Without protocol fee: + // Expected executed_surplus_fee is 167058994203399 + // + // With protocol fee: + // Expected executed_surplus_fee is 167058994203399 + + // 0.1*10000000000000000000 = 1000167058994203400 + // + // Final execution is 10000000000000000000 GNO for 8884257395945205588 DAI, with + // executed_surplus_fee = 1000167058994203400 GNO + // + // Settlement contract balance after execution = 1000167058994203400 GNO = + // 1000167058994203400 GNO * 8884257395945205588 / (10000000000000000000 - + // 1000167058994203400) = 987322948025407485 DAI + execute_test( + web3.clone(), + fee_policy, + OrderKind::Sell, + 1000167058994203400u128.into(), + 987322948025407485u128.into(), + ) + .await; +} + +async fn volume_fee_sell_order_test(web3: Web3) { + let fee_policy = FeePolicyKind::Volume { factor: 0.1 }; + // Without protocol fee: + // Expected executed_surplus_fee is 167058994203399 + // + // With protocol fee: + // Expected executed_surplus_fee is 167058994203399 + + // 0.1*10000000000000000000 = 1000167058994203400 + // + // Settlement contract balance after execution = 1000167058994203400 GNO = + // 1000167058994203400 GNO * 8884257395945205588 / (10000000000000000000 - + // 1000167058994203400) = 987322948025407485 DAI + execute_test( + web3.clone(), + fee_policy, + OrderKind::Sell, + 1000167058994203400u128.into(), + 987322948025407485u128.into(), + ) + .await; +} + +async fn price_improvement_fee_buy_order_test(web3: Web3) { + let fee_policy = FeePolicyKind::PriceImprovement { + factor: 0.3, + max_volume_factor: 1.0, + }; + // Without protocol fee: + // Expected execution is 5040413426236634210 GNO for 5000000000000000000 DAI, + // with executed_surplus_fee = 167058994203399 GNO + // + // With protocol fee: + // surplus in sell token = 10000000000000000000 - 5040413426236634210 = + // 4959586573763365790 + // + // protocol fee in sell token = 0.3*4959586573763365790 = 1487875972129009737 + // + // expected executed_surplus_fee is 167058994203399 + 1487875972129009737 = + // 1488043031123213136 + // + // Settlement contract balance after execution = executed_surplus_fee GNO + execute_test( + web3.clone(), + fee_policy, + OrderKind::Buy, + 1488043031123213136u128.into(), + 1488043031123213136u128.into(), + ) + .await; +} + +async fn price_improvement_fee_buy_order_capped_test(web3: Web3) { + let fee_policy = FeePolicyKind::PriceImprovement { + factor: 1.0, + max_volume_factor: 0.1, + }; + // Without protocol fee: + // Expected execution is 5040413426236634210 GNO for 5000000000000000000 DAI, + // with executed_surplus_fee = 167058994203399 GNO + // + // With protocol fee: + // Expected executed_surplus_fee is 167058994203399 + 0.1*5040413426236634210 = + // 504208401617866820 + // + // Settlement contract balance after execution = executed_surplus_fee GNO + execute_test( + web3.clone(), + fee_policy, + OrderKind::Buy, + 504208401617866820u128.into(), + 504208401617866820u128.into(), + ) + .await; +} + +async fn volume_fee_buy_order_test(web3: Web3) { + let fee_policy = FeePolicyKind::Volume { factor: 0.1 }; + // Without protocol fee: + // Expected execution is 5040413426236634210 GNO for 5000000000000000000 DAI, + // with executed_surplus_fee = 167058994203399 GNO + // + // With protocol fee: + // Expected executed_surplus_fee is 167058994203399 + 0.1*5040413426236634210 = + // 504208401617866820 + // + // Settlement contract balance after execution = executed_surplus_fee GNO + execute_test( + web3.clone(), + fee_policy, + OrderKind::Buy, + 504208401617866820u128.into(), + 504208401617866820u128.into(), + ) + .await; +} + +// because of rounding errors, it's good enough to check that the expected value +// is within a very narrow range of the executed value +fn is_approximately_equal(executed_value: U256, expected_value: U256) -> bool { + let lower = expected_value * U256::from(99999999999u128) / U256::from(100000000000u128); // in percents = 99.999999999% + let upper = expected_value * U256::from(100000000001u128) / U256::from(100000000000u128); // in percents = 100.000000001% + executed_value >= lower && executed_value <= upper +} + +async fn execute_test( + web3: Web3, + fee_policy: FeePolicyKind, + order_kind: OrderKind, + expected_surplus_fee: U256, + expected_settlement_contract_balance: U256, +) { + let mut onchain = OnchainComponents::deploy(web3.clone()).await; + + let [solver] = onchain.make_solvers(to_wei(1)).await; + let [trader] = onchain.make_accounts(to_wei(1)).await; + let [token_gno, token_dai] = onchain + .deploy_tokens_with_weth_uni_v2_pools(to_wei(1_000), to_wei(1000)) + .await; + + // Fund trader accounts + token_gno.mint(trader.address(), to_wei(100)).await; + + // Create and fund Uniswap pool + token_gno.mint(solver.address(), to_wei(1000)).await; + token_dai.mint(solver.address(), to_wei(1000)).await; + tx!( + solver.account(), + onchain + .contracts() + .uniswap_v2_factory + .create_pair(token_gno.address(), token_dai.address()) + ); + tx!( + solver.account(), + token_gno.approve( + onchain.contracts().uniswap_v2_router.address(), + to_wei(1000) + ) + ); + tx!( + solver.account(), + token_dai.approve( + onchain.contracts().uniswap_v2_router.address(), + to_wei(1000) + ) + ); + tx!( + solver.account(), + onchain.contracts().uniswap_v2_router.add_liquidity( + token_gno.address(), + token_dai.address(), + to_wei(1000), + to_wei(1000), + 0_u64.into(), + 0_u64.into(), + solver.address(), + U256::max_value(), + ) + ); + + // Approve GPv2 for trading + tx!( + trader.account(), + token_gno.approve(onchain.contracts().allowance, to_wei(100)) + ); + + // Place Orders + let services = Services::new(onchain.contracts()).await; + let solver_endpoint = + colocation::start_baseline_solver(onchain.contracts().weth.address()).await; + colocation::start_driver( + onchain.contracts(), + vec![SolverEngine { + name: "test_solver".into(), + account: solver, + endpoint: solver_endpoint, + }], + ); + services.start_autopilot(vec![ + "--drivers=test_solver|http://localhost:11088/test_solver".to_string(), + "--fee-policy-skip-market-orders=false".to_string(), + fee_policy.to_string(), + ]); + services + .start_api(vec![ + "--price-estimation-drivers=test_solver|http://localhost:11088/test_solver".to_string(), + ]) + .await; + + let order = OrderCreation { + sell_token: token_gno.address(), + sell_amount: to_wei(10), + buy_token: token_dai.address(), + buy_amount: to_wei(5), + valid_to: model::time::now_in_epoch_seconds() + 300, + kind: order_kind, + ..Default::default() + } + .sign( + EcdsaSigningScheme::Eip712, + &onchain.contracts().domain_separator, + SecretKeyRef::from(&SecretKey::from_slice(trader.private_key()).unwrap()), + ); + let uid = services.create_order(&order).await.unwrap(); + + // Drive solution + tracing::info!("Waiting for trade."); + wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 1 }) + .await + .unwrap(); + + wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 0 }) + .await + .unwrap(); + + onchain.mint_blocks_past_reorg_threshold().await; + let metadata_updated = || async { + onchain.mint_block().await; + let order = services.get_order(&uid).await.unwrap(); + is_approximately_equal(order.metadata.executed_surplus_fee, expected_surplus_fee) + }; + wait_for_condition(TIMEOUT, metadata_updated).await.unwrap(); + + // Check settlement contract balance + let balance_after = match order_kind { + OrderKind::Buy => token_gno + .balance_of(onchain.contracts().gp_settlement.address()) + .call() + .await + .unwrap(), + OrderKind::Sell => token_dai + .balance_of(onchain.contracts().gp_settlement.address()) + .call() + .await + .unwrap(), + }; + assert!(is_approximately_equal( + balance_after, + expected_settlement_contract_balance + )); +} + +enum FeePolicyKind { + /// How much of the order's price improvement over max(limit price, + /// best_bid) should be taken as a protocol fee. + PriceImprovement { factor: f64, max_volume_factor: f64 }, + /// How much of the order's volume should be taken as a protocol fee. + Volume { factor: f64 }, +} + +impl std::fmt::Display for FeePolicyKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + FeePolicyKind::PriceImprovement { + factor, + max_volume_factor, + } => write!( + f, + "--fee-policy-kind=priceImprovement:{}:{}", + factor, max_volume_factor + ), + FeePolicyKind::Volume { factor } => { + write!(f, "--fee-policy-kind=volume:{}", factor) + } + } + } +} From 7fccdcd49f9efbc2901ff82cb8f1645a0e08c23e Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 11 Jan 2024 20:20:59 +0100 Subject: [PATCH 35/35] log --- crates/driver/src/domain/competition/solution/fee.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index d19168eb0d..987260e404 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -82,6 +82,7 @@ impl Fulfillment { let price_improvement_fee = self.price_improvement_fee(prices, *factor)?; let max_volume_fee = self.volume_fee(prices, *max_volume_factor)?; // take the smaller of the two + tracing::debug!(uid=?self.order().uid, price_improvement_fee=?price_improvement_fee, max_volume_fee=?max_volume_fee, protocol_fee=?(std::cmp::min(price_improvement_fee, max_volume_fee)), executed=?self.executed(), surplus_fee=?self.surplus_fee(), "calculated protocol fee"); Ok(std::cmp::min(price_improvement_fee, max_volume_fee)) } Some(FeePolicy::Volume { factor }) => self.volume_fee(prices, *factor),