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..987260e404 --- /dev/null +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -0,0 +1,218 @@ +//! 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 (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 +//! 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 = 10 USDC (always expressed in sell token) +//! Executed = 1 WETH (always expressed in target token) +//! +//! This response is adjusted by the protocol fee of 5 USDC: +//! Fee = 10 USDC + 5 USDC = 15 USDC +//! Executed = 1 WETH + +use { + super::trade::{Fee, Fulfillment, InvalidExecutedAmount}, + crate::domain::{ + competition::{ + order, + order::{FeePolicy, Side}, + }, + eth, + }, +}; + +impl Fulfillment { + /// 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 + let fee = match self.surplus_fee() { + None => { + if !protocol_fee.is_zero() { + return Err(Error::ProtocolFeeOnStaticOrder); + } + Fee::Static + } + Some(fee) => { + Fee::Dynamic((fee.0.checked_add(protocol_fee).ok_or(Error::Overflow)?).into()) + } + }; + + // 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 their own fee. + let order = self.order().clone(); + let executed = match order.side { + order::Side::Buy => self.executed(), + order::Side::Sell => order::TargetAmount( + self.executed() + .0 + .checked_sub(protocol_fee) + .ok_or(Error::Overflow)?, + ), + }; + + Fulfillment::new(order, executed, fee).map_err(Into::into) + } + + fn protocol_fee(&self, prices: ClearingPrices) -> Result { + // TODO: support multiple fee policies + if self.order().fee_policies.len() > 1 { + return Err(Error::MultipleFeePolicies); + } + + 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 + 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), + None => Ok(0.into()), + } + } + + 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; + 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` + executed + .checked_mul(prices.buy) + .ok_or(Error::Overflow)? + .checked_div(prices.sell) + .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) + .ok_or(Error::Overflow)? + .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 + limit_sell_amount + .checked_sub(executed_sell_amount_with_fee) + .unwrap_or(eth::U256::zero()) + } + Side::Sell => { + // 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()); + // surplus in sell token + surplus + .checked_mul(prices.buy) + .ok_or(Error::Overflow)? + .checked_div(prices.sell) + .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 executed_sell_amount = match self.order().side { + Side::Buy => { + // How much `sell_token` we need to sell to buy `executed` amount of `buy_token` + executed + .checked_mul(prices.buy) + .ok_or(Error::Overflow)? + .checked_div(prices.sell) + .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 { + pub sell: eth::U256, + pub buy: eth::U256, +} + +#[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")] + DivisionByZero, + #[error(transparent)] + InvalidExecutedAmount(#[from] InvalidExecutedAmount), +} diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 716c7b994e..8b8b33273c 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::{ @@ -18,6 +19,7 @@ use { thiserror::Error, }; +pub mod fee; pub mod interaction; pub mod settlement; pub mod trade; @@ -49,7 +51,7 @@ impl Solution { solver: Solver, score: SolverScore, weth: eth::WethAddress, - ) -> Result { + ) -> Result { let solution = Self { id, trades, @@ -65,9 +67,9 @@ 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) + Err(SolutionError::InvalidClearingPrices) } } @@ -176,6 +178,29 @@ impl Solution { Settlement::encode(self, auction, eth, simulator).await } + pub fn with_protocol_fees(self) -> Result { + let mut trades = Vec::with_capacity(self.trades.len()); + 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)], + }; + let fulfillment = fulfillment.with_protocol_fee(prices)?; + trades.push(Trade::Fulfillment(fulfillment)) + } + order::Kind::Liquidity => { + trades.push(trade); + } + }, + Trade::Jit(_) => trades.push(trade), + } + } + Ok(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. @@ -279,8 +304,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" )] @@ -293,6 +316,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)] + 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 9810ce7232..6fd642430a 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -35,7 +35,7 @@ impl Fulfillment { // 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(), @@ -43,13 +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 + surplus_fee.0) <= available - } - order::Partial::No => { - order::TargetAmount(executed.0 + surplus_fee.0) == order.target() - } + order::Partial::Yes { available } => executed_with_fee <= available, + order::Partial::No => executed_with_fee == order.target(), } }; @@ -97,6 +95,14 @@ impl Fulfillment { } } + /// Returns the solver determined fee if it exists. + pub fn surplus_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. pub fn sell_amount( &self, @@ -195,11 +201,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/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 2815667371..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( @@ -206,8 +208,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".to_owned()) + } + competition::solution::SolutionError::ProtocolFee(err) => { + super::Error(format!("could not incorporate protocol fee: {err}")) + } }) }) .collect() diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 75c26e0d12..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.5, - "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() { diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index c575ec1b01..a7d2f6f0e2 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -79,7 +79,7 @@ impl Default for Score { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct Order { pub name: &'static str, @@ -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, 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) + } + } + } +}