From 5b2374ceb17679972c057d4bd1c9bb45adfdafa1 Mon Sep 17 00:00:00 2001 From: ilya Date: Wed, 30 Oct 2024 12:37:08 +0000 Subject: [PATCH] Compute jit orders net changes --- .../src/price_estimation/trade_verifier.rs | 519 +++++++++++++++++- crates/shared/src/trade_finding.rs | 52 +- 2 files changed, 535 insertions(+), 36 deletions(-) diff --git a/crates/shared/src/price_estimation/trade_verifier.rs b/crates/shared/src/price_estimation/trade_verifier.rs index 59019185fe..7ab8c3b405 100644 --- a/crates/shared/src/price_estimation/trade_verifier.rs +++ b/crates/shared/src/price_estimation/trade_verifier.rs @@ -8,6 +8,7 @@ use { trade_finding::{external::dto, Interaction, TradeKind}, }, anyhow::{Context, Result}, + bigdecimal::{Signed, ToPrimitive, Zero}, contracts::{ deployed_bytecode, dummy_contract, @@ -631,34 +632,78 @@ fn ensure_quote_accuracy( query: &PriceQuery, trade: &TradeKind, summary: &SettleOutput, -) -> Result { +) -> std::result::Result { // amounts verified by the simulation let (sell_amount, buy_amount) = match query.kind { OrderKind::Buy => (summary.out_amount, query.in_amount.get()), OrderKind::Sell => (query.in_amount.get(), summary.out_amount), }; + let (sell_amount, buy_amount) = ( + u256_to_big_rational(&sell_amount), + u256_to_big_rational(&buy_amount), + ); let (expected_sell_token_lost, expected_buy_token_lost) = match trade { TradeKind::Regular(trade) => { - let jit_orders_executed_amounts = trade.jit_orders_executed_amounts()?; - let expected_sell_token_lost = sell_amount - - jit_orders_executed_amounts - .get(&query.sell_token) - .context("missing jit orders sell token executed amount")?; - let expected_buy_token_lost = buy_amount - - jit_orders_executed_amounts + let jit_orders_net_token_changes = trade.jit_orders_net_token_changes()?; + let jit_orders_sell_token_changes = jit_orders_net_token_changes + .get(&query.sell_token) + .context("missing jit orders sell token executed amount")?; + let jit_orders_buy_token_changes = + jit_orders_net_token_changes .get(&query.buy_token) .context("missing jit orders buy token executed amount")?; - (expected_sell_token_lost, expected_buy_token_lost) + // sell-sell + // jit_orders_sell_token_changes: -400.0 + // jit_orders_buy_token_changes: 200.0 + // sell_amount: 500.0 + // buy_amount: 250.0 + + // buy-buy + // jit_orders_sell_token_changes: 400.0 + // jit_orders_buy_token_changes: -200.0 + // sell_amount: 500.0 + // buy_amount: 250.0 + + // buy-sell + // jit_orders_sell_token_changes: -400.0 + // jit_orders_buy_token_changes: 200.0 + // sell_amount: 500.0 + // buy_amount: 250.0 + + // sell-buy + // jit_orders_sell_token_changes: 400.0 + // jit_orders_buy_token_changes: -200.0 + // sell_amount: 500.0 + // buy_amount: 250.0 + + match query.kind { + OrderKind::Sell => { + let expected_sell_token_lost = -&sell_amount - jit_orders_sell_token_changes; + let expected_buy_token_lost = &buy_amount - jit_orders_buy_token_changes; + (expected_sell_token_lost, expected_buy_token_lost) + } + OrderKind::Buy => { + let expected_sell_token_lost = -(&sell_amount - jit_orders_sell_token_changes); + let expected_buy_token_lost = &buy_amount + jit_orders_buy_token_changes; + (expected_sell_token_lost, expected_buy_token_lost) + } + } } - TradeKind::Legacy(_) => (sell_amount, buy_amount), + TradeKind::Legacy(_) => (BigRational::zero(), BigRational::zero()), }; - if summary.sell_tokens_lost - >= inaccuracy_limit * u256_to_big_rational(&expected_sell_token_lost) - || summary.buy_tokens_lost - >= inaccuracy_limit * u256_to_big_rational(&expected_buy_token_lost) - { + let sell_token_lost = (&summary.sell_tokens_lost - expected_sell_token_lost) + .abs() + .to_f64() + .unwrap(); + let sell_token_lost_limit = (inaccuracy_limit * &sell_amount).to_f64().unwrap(); + let buy_token_lost = (&summary.buy_tokens_lost - expected_buy_token_lost) + .abs() + .to_f64() + .unwrap(); + let buy_token_lost_limit = (inaccuracy_limit * &buy_amount).to_f64().unwrap(); + if sell_token_lost >= sell_token_lost_limit || buy_token_lost >= buy_token_lost_limit { return Err(Error::TooInaccurate); } @@ -692,7 +737,13 @@ enum Error { #[cfg(test)] mod tests { - use super::*; + use { + super::*, + crate::trade_finding::Trade, + app_data::AppDataHash, + bigdecimal::FromPrimitive, + model::order::{BuyTokenDestination, SellTokenSource}, + }; #[test] fn discards_inaccurate_quotes() { @@ -757,4 +808,440 @@ mod tests { let estimate = ensure_quote_accuracy(&low_threshold, &query, &trade, &pay_out_less); assert!(estimate.is_ok()); } + + #[test] + fn test_ensure_quote_accuracy_with_jit_orders_partial_sell_sell() { + // Inaccuracy limit of 10% + let low_threshold = BigRational::from_float(0.1).unwrap(); + let high_threshold = BigRational::from_float(0.11).unwrap(); + + let sell_token: H160 = H160::from_low_u64_be(1); + let buy_token: H160 = H160::from_low_u64_be(2); + + // User wants to sell 500 units of Token A for Token B + let query = PriceQuery { + in_amount: NonZeroU256::new(500u64.into()).unwrap(), + kind: OrderKind::Sell, + sell_token, + buy_token, + }; + + // Clearing prices: Token A = 1, Token B = 2 + let mut clearing_prices = HashMap::new(); + clearing_prices.insert(sell_token, U256::from(1u64)); + clearing_prices.insert(buy_token, U256::from(2u64)); + + // JIT order partially covers the user's trade + let jit_order = dto::JitOrder { + sell_token: buy_token, // Solver sells Token B + buy_token: sell_token, // Solver buys Token A + executed_amount: U256::from(200u64), + side: dto::Side::Sell, + sell_amount: U256::from(200u64), + buy_amount: U256::from(400u64), + receiver: H160::zero(), + valid_to: 0, + app_data: AppDataHash::default(), + partially_fillable: false, + sell_token_source: SellTokenSource::Erc20, + buy_token_destination: BuyTokenDestination::Erc20, + signature: vec![], + signing_scheme: SigningScheme::Eip1271, + }; + + let trade_kind = TradeKind::Regular(Trade { + clearing_prices: clearing_prices.clone(), + gas_estimate: Some(50_000), + pre_interactions: vec![], + interactions: vec![], + solver: H160::from_low_u64_be(0x1234), + tx_origin: None, + jit_orders: vec![jit_order], + }); + + // Simulation summary with zero net changes for the settlement contract + let summary = SettleOutput { + gas_used: U256::from(50_000u64), + // The out amount still covers the full query amount + out_amount: U256::from(250u64), + // Since the JIT order covered only 200 units of Token B, the settlement contract is + // expected to lose the remaining 50 units. Let's lose a bit more. + buy_tokens_lost: BigRational::from_u64(75).unwrap(), + // And the settlement contract is expected to receive 100 units of Token A. Let's gain a + // bit more. + sell_tokens_lost: BigRational::from_i64(-150).unwrap(), + }; + + // The summary has 10% inaccuracy + let estimate = ensure_quote_accuracy(&low_threshold, &query, &trade_kind, &summary); + assert!(matches!(estimate, Err(Error::TooInaccurate))); + + // The summary has less than 11% inaccuracy + let estimate = + ensure_quote_accuracy(&high_threshold, &query, &trade_kind, &summary).unwrap(); + assert!(estimate.verified); + } + + #[test] + fn test_ensure_quote_accuracy_with_jit_orders_sell_sell() { + // Inaccuracy limit of 10% + let low_threshold = BigRational::from_float(0.1).unwrap(); + let high_threshold = BigRational::from_float(0.11).unwrap(); + + let sell_token: H160 = H160::from_low_u64_be(1); + let buy_token: H160 = H160::from_low_u64_be(2); + + // User wants to sell 500 units of Token A for Token B + let query = PriceQuery { + in_amount: NonZeroU256::new(500u64.into()).unwrap(), + kind: OrderKind::Sell, + sell_token, + buy_token, + }; + + // Clearing prices: Token A = 1, Token B = 2 + let mut clearing_prices = HashMap::new(); + clearing_prices.insert(sell_token, U256::from(1u64)); + clearing_prices.insert(buy_token, U256::from(2u64)); + + // JIT order fully covers the user's trade + let jit_order = dto::JitOrder { + sell_token: buy_token, // Solver sells Token B + buy_token: sell_token, // Solver buys Token A + executed_amount: U256::from(250u64), + side: dto::Side::Sell, + sell_amount: U256::from(250u64), + buy_amount: U256::from(500u64), + receiver: H160::zero(), + valid_to: 0, + app_data: AppDataHash::default(), + partially_fillable: false, + sell_token_source: SellTokenSource::Erc20, + buy_token_destination: BuyTokenDestination::Erc20, + signature: vec![], + signing_scheme: SigningScheme::Eip1271, + }; + + let trade_kind = TradeKind::Regular(Trade { + clearing_prices: clearing_prices.clone(), + gas_estimate: Some(50_000), + pre_interactions: vec![], + interactions: vec![], + solver: H160::from_low_u64_be(0x1234), + tx_origin: None, + jit_orders: vec![jit_order], + }); + + // Simulation summary with zero net changes for the settlement contract + let summary = SettleOutput { + gas_used: U256::from(50_000u64), + // The out amount still covers the full query amount + out_amount: U256::from(250u64), + // Since the JIT order fully covered the requested amount, the settlement contract + // should not have any net changes but we add some withing the 11% limit. + buy_tokens_lost: BigRational::from_u64(25).unwrap(), + sell_tokens_lost: BigRational::from_i64(50).unwrap(), + }; + + // The summary has 10% inaccuracy + let estimate = ensure_quote_accuracy(&low_threshold, &query, &trade_kind, &summary); + assert!(matches!(estimate, Err(Error::TooInaccurate))); + + // The summary has less than 11% inaccuracy + let estimate = + ensure_quote_accuracy(&high_threshold, &query, &trade_kind, &summary).unwrap(); + assert!(estimate.verified); + } + + #[test] + fn test_ensure_quote_accuracy_with_jit_orders_partial_buy_buy() { + // Inaccuracy limit of 10% + let low_threshold = BigRational::from_float(0.1).unwrap(); + let high_threshold = BigRational::from_float(0.11).unwrap(); + + let sell_token: H160 = H160::from_low_u64_be(1); + let buy_token: H160 = H160::from_low_u64_be(2); + + // User wants to buy 250 units of Token B using Token A + let query = PriceQuery { + in_amount: NonZeroU256::new(250u64.into()).unwrap(), + kind: OrderKind::Buy, + sell_token, + buy_token, + }; + + // Clearing prices: Token A = 1, Token B = 2 + let mut clearing_prices = HashMap::new(); + clearing_prices.insert(sell_token, U256::from(1u64)); + clearing_prices.insert(buy_token, U256::from(2u64)); + + // JIT order partially covers the user's trade + let jit_order = dto::JitOrder { + sell_token: buy_token, // Solver sell Token B + buy_token: sell_token, // Solver buys Token A + executed_amount: U256::from(400u64), + side: dto::Side::Buy, + sell_amount: U256::from(200u64), + buy_amount: U256::from(400u64), + receiver: H160::zero(), + valid_to: 0, + app_data: AppDataHash::default(), + partially_fillable: false, + sell_token_source: SellTokenSource::Erc20, + buy_token_destination: BuyTokenDestination::Erc20, + signature: vec![], + signing_scheme: SigningScheme::Eip1271, + }; + + let trade_kind = TradeKind::Regular(Trade { + clearing_prices: clearing_prices.clone(), + gas_estimate: Some(50_000), + pre_interactions: vec![], + interactions: vec![], + solver: H160::from_low_u64_be(0x1234), + tx_origin: None, + jit_orders: vec![jit_order], + }); + + // Simulation summary with net changes for the settlement contract + let summary = SettleOutput { + gas_used: U256::from(50_000u64), + // The out amount is the total amount of buy token the user gets + out_amount: U256::from(500u64), + // Settlement contract covers the remaining 50 units of Token B, but to test the 11% + // limit we lose a bit more. + buy_tokens_lost: BigRational::from_u64(75).unwrap(), + // Settlement contract gains 100 units of Token A. To test the 11% limit we gain a bit + // more. + sell_tokens_lost: BigRational::from_i64(-150).unwrap(), + }; + + // The summary has 10% inaccuracy + let estimate = ensure_quote_accuracy(&low_threshold, &query, &trade_kind, &summary); + assert!(matches!(estimate, Err(Error::TooInaccurate))); + + // The summary has less than 11% inaccuracy + let estimate = + ensure_quote_accuracy(&high_threshold, &query, &trade_kind, &summary).unwrap(); + assert!(estimate.verified); + } + + #[test] + fn test_ensure_quote_accuracy_with_jit_orders_buy_buy() { + // Inaccuracy limit of 10% + let low_threshold = BigRational::from_float(0.1).unwrap(); + let high_threshold = BigRational::from_float(0.11).unwrap(); + + let sell_token: H160 = H160::from_low_u64_be(1); + let buy_token: H160 = H160::from_low_u64_be(2); + + // User wants to buy 250 units of Token B using Token A + let query = PriceQuery { + in_amount: NonZeroU256::new(250u64.into()).unwrap(), + kind: OrderKind::Buy, + sell_token, + buy_token, + }; + + // Clearing prices: Token A = 1, Token B = 2 + let mut clearing_prices = HashMap::new(); + clearing_prices.insert(sell_token, U256::from(1u64)); + clearing_prices.insert(buy_token, U256::from(2u64)); + + // JIT order fully covers the user's trade + let jit_order = dto::JitOrder { + sell_token: buy_token, // Solver sell Token B + buy_token: sell_token, // Solver buys Token A + executed_amount: U256::from(500u64), + side: dto::Side::Buy, + sell_amount: U256::from(250u64), + buy_amount: U256::from(500u64), + receiver: H160::zero(), + valid_to: 0, + app_data: AppDataHash::default(), + partially_fillable: false, + sell_token_source: SellTokenSource::Erc20, + buy_token_destination: BuyTokenDestination::Erc20, + signature: vec![], + signing_scheme: SigningScheme::Eip1271, + }; + + let trade_kind = TradeKind::Regular(Trade { + clearing_prices: clearing_prices.clone(), + gas_estimate: Some(50_000), + pre_interactions: vec![], + interactions: vec![], + solver: H160::from_low_u64_be(0x1234), + tx_origin: None, + jit_orders: vec![jit_order], + }); + + // Simulation summary with net changes for the settlement contract + let summary = SettleOutput { + gas_used: U256::from(50_000u64), + // The out amount is the total amount of buy token the user gets + out_amount: U256::from(500u64), + // Settlement contract balance shouldn't change, but to test the 11% limit we lose a + // bit. + buy_tokens_lost: BigRational::from_u64(25).unwrap(), + // Settlement contract balance shouldn't change, but to test the 11% limit we gain a + // bit. + sell_tokens_lost: BigRational::from_i64(-50).unwrap(), + }; + + // The summary has 10% inaccuracy + let estimate = ensure_quote_accuracy(&low_threshold, &query, &trade_kind, &summary); + assert!(matches!(estimate, Err(Error::TooInaccurate))); + + // The summary has less than 11% inaccuracy + let estimate = + ensure_quote_accuracy(&high_threshold, &query, &trade_kind, &summary).unwrap(); + assert!(estimate.verified); + } + + #[test] + fn test_ensure_quote_accuracy_with_jit_orders_partial_buy_sell() { + // Inaccuracy limit of 10% + let low_threshold = BigRational::from_float(0.1).unwrap(); + let high_threshold = BigRational::from_float(0.11).unwrap(); + + let sell_token: H160 = H160::from_low_u64_be(1); + let buy_token: H160 = H160::from_low_u64_be(2); + + // User wants to buy 250 units of Token B using Token A + let query = PriceQuery { + in_amount: NonZeroU256::new(250u64.into()).unwrap(), + kind: OrderKind::Buy, + sell_token, + buy_token, + }; + + // Clearing prices: Token A = 1, Token B = 2 + let mut clearing_prices = HashMap::new(); + clearing_prices.insert(sell_token, U256::from(1u64)); + clearing_prices.insert(buy_token, U256::from(2u64)); + + // JIT order partially covers the user's trade + let jit_order = dto::JitOrder { + sell_token: buy_token, // Solver sells Token B + buy_token: sell_token, // Solver buys Token A + executed_amount: U256::from(200u64), + side: dto::Side::Sell, + sell_amount: U256::from(200u64), + buy_amount: U256::from(400u64), + receiver: H160::zero(), + valid_to: 0, + app_data: AppDataHash::default(), + partially_fillable: false, + sell_token_source: SellTokenSource::Erc20, + buy_token_destination: BuyTokenDestination::Erc20, + signature: vec![], + signing_scheme: SigningScheme::Eip1271, + }; + + let trade_kind = TradeKind::Regular(Trade { + clearing_prices: clearing_prices.clone(), + gas_estimate: Some(50_000), + pre_interactions: vec![], + interactions: vec![], + solver: H160::from_low_u64_be(0x1234), + tx_origin: None, + jit_orders: vec![jit_order], + }); + + // Simulation summary with net changes for the settlement contract + let summary = SettleOutput { + gas_used: U256::from(50_000u64), + // The out amount is the total amount of buy token the user gets + out_amount: U256::from(500u64), + // Settlement contract covers the remaining 50 units of Token B, but to test the 11% + // limit we lose a bit more. + buy_tokens_lost: BigRational::from_u64(75).unwrap(), + // Settlement contract gains 100 units of Token A. To test the 11% limit we gain a bit + // more. + sell_tokens_lost: BigRational::from_i64(-150).unwrap(), + }; + + // The summary has 10% inaccuracy + let estimate = ensure_quote_accuracy(&low_threshold, &query, &trade_kind, &summary); + assert!(matches!(estimate, Err(Error::TooInaccurate))); + + // The summary has less than 11% inaccuracy + let estimate = + ensure_quote_accuracy(&high_threshold, &query, &trade_kind, &summary).unwrap(); + assert!(estimate.verified); + } + + #[test] + fn test_ensure_quote_accuracy_with_jit_orders_partial_sell_buy() { + // Inaccuracy limit of 10% + let low_threshold = BigRational::from_float(0.1).unwrap(); + let high_threshold = BigRational::from_float(0.11).unwrap(); + + let sell_token: H160 = H160::from_low_u64_be(1); + let buy_token: H160 = H160::from_low_u64_be(2); + + // User wants to sell 500 units of Token A for Token B + let query = PriceQuery { + in_amount: NonZeroU256::new(500u64.into()).unwrap(), + kind: OrderKind::Sell, + sell_token, + buy_token, + }; + + // Clearing prices: Token A = 1, Token B = 2 + let mut clearing_prices = HashMap::new(); + clearing_prices.insert(sell_token, U256::from(1u64)); + clearing_prices.insert(buy_token, U256::from(2u64)); + + // JIT order partially covers the user's trade + let jit_order = dto::JitOrder { + sell_token: buy_token, // Solver sell Token B + buy_token: sell_token, // Solver buys Token A + executed_amount: U256::from(400u64), + side: dto::Side::Buy, + sell_amount: U256::from(200u64), + buy_amount: U256::from(400u64), + receiver: H160::zero(), + valid_to: 0, + app_data: AppDataHash::default(), + partially_fillable: false, + sell_token_source: SellTokenSource::Erc20, + buy_token_destination: BuyTokenDestination::Erc20, + signature: vec![], + signing_scheme: SigningScheme::Eip1271, + }; + + let trade_kind = TradeKind::Regular(Trade { + clearing_prices: clearing_prices.clone(), + gas_estimate: Some(50_000), + pre_interactions: vec![], + interactions: vec![], + solver: H160::from_low_u64_be(0x1234), + tx_origin: None, + jit_orders: vec![jit_order], + }); + + // Simulation summary with net changes for the settlement contract + let summary = SettleOutput { + gas_used: U256::from(50_000u64), + // The out amount is the total amount of buy token the user gets + out_amount: U256::from(250u64), + // Settlement contract covers the remaining 50 units of Token B, but to test the 11% + // limit we lose a bit more. + buy_tokens_lost: BigRational::from_u64(75).unwrap(), + // Settlement contract gains 100 units of Token A. To test the 11% limit we gain a bit + // more. + sell_tokens_lost: BigRational::from_i64(-150).unwrap(), + }; + + // The summary has 10% inaccuracy + let estimate = ensure_quote_accuracy(&low_threshold, &query, &trade_kind, &summary); + assert!(matches!(estimate, Err(Error::TooInaccurate))); + + // The summary has less than 11% inaccuracy + let estimate = + ensure_quote_accuracy(&high_threshold, &query, &trade_kind, &summary).unwrap(); + assert!(estimate.verified); + } } diff --git a/crates/shared/src/trade_finding.rs b/crates/shared/src/trade_finding.rs index 4fee8ad264..cbc7a92812 100644 --- a/crates/shared/src/trade_finding.rs +++ b/crates/shared/src/trade_finding.rs @@ -13,7 +13,7 @@ use { derivative::Derivative, ethcontract::{contract::MethodBuilder, tokens::Tokenize, web3::Transport, Bytes, H160, U256}, model::{interaction::InteractionData, order::OrderKind}, - num::CheckedDiv, + num::{BigRational, CheckedDiv}, number::conversions::big_rational_to_u256, serde::Serialize, std::{collections::HashMap, ops::Mul}, @@ -171,8 +171,8 @@ impl Trade { big_rational_to_u256(&out_amount).context("out amount is not a valid U256") } - pub fn jit_orders_executed_amounts(&self) -> Result> { - let mut executed_amounts: HashMap = HashMap::new(); + pub fn jit_orders_net_token_changes(&self) -> Result> { + let mut net_token_changes: HashMap = HashMap::new(); for jit_order in self.jit_orders.iter() { let sell_price = self @@ -186,6 +186,7 @@ impl Trade { .context("JIT order buy token clearing price is missing")? .to_big_rational(); let executed_amount = jit_order.executed_amount.to_big_rational(); + let (executed_sell, executed_buy) = match jit_order.side { Side::Sell => { let buy_amount = executed_amount @@ -193,7 +194,7 @@ impl Trade { .mul(&sell_price) .checked_div(&buy_price) .context("division by zero in JIT order sell")?; - (executed_amount, buy_amount) + (executed_amount.clone(), buy_amount) } Side::Buy => { let sell_amount = executed_amount @@ -201,26 +202,37 @@ impl Trade { .mul(&buy_price) .checked_div(&sell_price) .context("division by zero in JIT order buy")?; - (sell_amount, executed_amount) + (sell_amount, executed_amount.clone()) } }; - let (executed_sell, executed_buy) = ( - big_rational_to_u256(&executed_sell) - .context("executed sell amount is not a valid U256")?, - big_rational_to_u256(&executed_buy) - .context("executed buy amount is not a valid U256")?, - ); - executed_amounts - .entry(jit_order.sell_token) - .and_modify(|executed| *executed += executed_sell) - .or_insert(executed_sell); - executed_amounts - .entry(jit_order.buy_token) - .and_modify(|executed| *executed += executed_buy) - .or_insert(executed_buy); + + match jit_order.side { + Side::Sell => { + net_token_changes + .entry(jit_order.sell_token) + .and_modify(|e| *e += executed_sell.clone()) + .or_insert(executed_sell.clone()); + + net_token_changes + .entry(jit_order.buy_token) + .and_modify(|e| *e -= executed_buy.clone()) + .or_insert(-executed_buy.clone()); + } + Side::Buy => { + net_token_changes + .entry(jit_order.sell_token) + .and_modify(|e| *e -= executed_sell.clone()) + .or_insert(-executed_sell.clone()); + + net_token_changes + .entry(jit_order.buy_token) + .and_modify(|e| *e += executed_buy.clone()) + .or_insert(executed_buy.clone()); + } + } } - Ok(executed_amounts) + Ok(net_token_changes) } }