Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions crates/driver/src/domain/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ pub struct Quote {

impl Quote {
fn try_new(eth: &Ethereum, solution: competition::Solution) -> Result<Self, Error> {
let clearing_prices: HashMap<eth::Address, eth::U256> = solution
.clearing_prices()
.into_iter()
.map(|(token, amount)| (token.into(), amount))
.collect();

Ok(Self {
clearing_prices: solution
.clearing_prices()
.into_iter()
.map(|(token, amount)| (token.into(), amount))
.collect(),
clearing_prices,
pre_interactions: solution.pre_interactions().to_vec(),
interactions: solution
.interactions()
Expand Down
1 change: 1 addition & 0 deletions crates/driver/src/infra/config/file/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ pub async fn load(chain: Chain, path: &Path) -> infra::Config {
file::AtBlock::Latest => liquidity::AtBlock::Latest,
file::AtBlock::Finalized => liquidity::AtBlock::Finalized,
},
margin_bps: solver_config.margin_bps,
}
}))
.await,
Expand Down
13 changes: 13 additions & 0 deletions crates/driver/src/infra/config/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,19 @@ struct SolverConfig {
/// before the driver starts dropping new `/solve` requests.
#[serde(default = "default_settle_queue_size")]
settle_queue_size: usize,

/// Margin in basis points (0-10000). Applied to order limits sent to
/// solvers to make bids more conservative. For sell orders, increases
/// the minimum buy amount requirement. For buy orders, decreases the
/// maximum sell amount allowed. This forces solvers to find solutions
/// with enough surplus to cover the margin. Default: 0 (no margin).
///
/// Note: This value should typically match the slippage tolerance
/// configured on external pricing APIs used by the solver. Setting a
/// higher margin than the expected negative slippage would unnecessarily
/// penalize orders.
#[serde(default)]
margin_bps: u32,
}

#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
Expand Down
39 changes: 39 additions & 0 deletions crates/driver/src/infra/solver/dto/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub fn new(
flashloan_hints: &HashMap<order::Uid, eth::Flashloan>,
wrappers: &WrapperCalls,
deadline: chrono::DateTime<chrono::Utc>,
margin_bps: u32,
) -> solvers_dto::auction::Auction {
let mut tokens: HashMap<eth::Address, _> = auction
.tokens()
Expand Down Expand Up @@ -110,6 +111,44 @@ pub fn new(
}
})
}
// Apply margin by adjusting order limits (similar to volume fees).
// This forces solvers to find solutions with enough surplus to cover
// the margin applied during competition scoring.
if margin_bps > 0 {
let factor = margin_bps as f64 / 10_000.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using f64 for financial calculations like factor = margin_bps as f64 / 10_000.0 can introduce floating-point precision errors. It's generally safer and more accurate to perform these calculations using integer arithmetic with U256 or BigRational to avoid unexpected rounding issues, especially when dealing with token amounts.

                    let factor = eth::U256::from(margin_bps);
                    let denominator = eth::U256::from(10_000);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The apply_factor accepts f64, so it should be fine I guess.

match order.side {
Side::Buy => {
// For buy orders: reduce maximum sell amount
if let Some(adjusted) =
available.sell.amount.apply_factor(1.0 / (1.0 + factor))
{
available.sell.amount = adjusted;
} else {
tracing::warn!(
"applying margin bps {} led to sell amount underflow for \
order {:?}",
margin_bps,
order.uid
);
}
}
Side::Sell => {
// For sell orders: increase minimum buy amount requirement
if let Some(adjusted) =
available.buy.amount.apply_factor(1.0 / (1.0 - factor))
{
available.buy.amount = adjusted;
Comment on lines +137 to +140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The calculation 1.0 / (1.0 - factor) for sell orders can lead to division by zero if margin_bps is 10000 (i.e., factor is 1.0). In this scenario, apply_factor will return None due to floating-point division by zero resulting in inf, which cannot be converted to U256. The current implementation then logs a warning and proceeds with the original available.buy.amount, effectively ignoring the 100% margin. This is a correctness issue, as a 100% margin should make the order unfillable or require an impossibly large buy amount, not simply bypass the margin.

Consider adding a check to ensure margin_bps is strictly less than 10000 to prevent this edge case, or explicitly handle the None result from apply_factor as an unfillable order rather than silently using the unadjusted amount.

                            if margin_bps >= 10000 { // Handle 100% margin as an impossible order
                                tracing::warn!(
                                    "applying margin bps {} led to infinite buy amount for order \n                                     {:?}, treating as unfillable",
                                    margin_bps,
                                    order.uid
                                );
                                available.buy.amount = eth::U256::MAX; // Or some other indicator of unfillable
                            } else if let Some(adjusted) =
                                available.buy.amount.apply_factor(1.0 / (1.0 - factor))
                            {
                                available.buy.amount = adjusted;
                            } else {
                                tracing::warn!(
                                    "applying margin bps {} led to buy amount overflow for order \n                                     {:?}",
                                    margin_bps,
                                    order.uid
                                );
                            }

} else {
tracing::warn!(
"applying margin bps {} led to buy amount overflow for order \
{:?}",
margin_bps,
order.uid
);
}
}
}
}
solvers_dto::auction::Order {
uid: order.uid.into(),
sell_token: available.sell.token.0.0,
Expand Down
11 changes: 11 additions & 0 deletions crates/driver/src/infra/solver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ pub struct Config {
/// Defines at which block the liquidity needs to be fetched on /solve
/// requests.
pub fetch_liquidity_at_block: infra::liquidity::AtBlock,
/// Margin in basis points (0-10000). Applied to order limits sent to
/// solvers to make bids more conservative. For sell orders, increases
/// the minimum buy amount. For buy orders, decreases the maximum sell
/// amount. Default: 0 (no margin).
pub margin_bps: u32,
}

impl Solver {
Expand Down Expand Up @@ -277,6 +282,11 @@ impl Solver {
self.config.fetch_liquidity_at_block.clone()
}

/// Margin in basis points (0-10000) for conservative bidding.
pub fn margin_bps(&self) -> u32 {
self.config.margin_bps
}

/// Make a POST request instructing the solver to solve an auction.
/// Allocates at most `timeout` time for the solving.
#[instrument(name = "solver_engine", skip_all)]
Expand All @@ -301,6 +311,7 @@ impl Solver {
&flashloan_hints,
&wrappers,
auction.deadline(self.timeouts()).solvers(),
self.config.margin_bps,
);

let body = {
Expand Down
47 changes: 47 additions & 0 deletions crates/driver/src/tests/cases/margin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Tests that verify margin is correctly applied to order limits before
//! sending to solvers.

use crate::{
domain::{competition::order, eth},
tests::{
self,
setup::{ab_order, ab_pool, ab_solution, test_solver},
},
};

/// Test that verifies the solver receives orders with adjusted limits when
/// margin is applied.
///
/// For sell orders: the minimum buy amount is increased by the margin factor
/// For buy orders: the maximum sell amount is reduced by the margin factor
///
/// The test works by setting up a solver mock that expects the adjusted amounts
/// and will fail the assertion if the received amounts don't match.
#[tokio::test]
#[ignore]
async fn margin_adjusts_order_limits() {
// Test with 1% margin (100 basis points)
let margin_bps = 100u32;

for side in [order::Side::Sell, order::Side::Buy] {
// Limit orders require solver-determined fees
let order = ab_order()
.kind(order::Kind::Limit)
.side(side)
.solver_fee(Some(eth::U256::from(500)));

let test = tests::setup()
.name(format!("Margin: {side:?}"))
.solvers(vec![test_solver().margin_bps(margin_bps)])
.pool(ab_pool())
.order(order.clone())
.solution(ab_solution())
.done()
.await;

// The solver mock will verify that the order limits are adjusted
// according to the margin. If the limits are not correctly adjusted,
// the mock will fail with an assertion error.
test.solve().await.ok().orders(&[order]);
}
}
1 change: 1 addition & 0 deletions crates/driver/src/tests/cases/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod fees;
mod flashloan_hints;
pub mod internalization;
pub mod jit_orders;
pub mod margin;
pub mod merge_settlements;
pub mod multiple_drivers;
pub mod multiple_solutions;
Expand Down
2 changes: 2 additions & 0 deletions crates/driver/src/tests/setup/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ async fn create_config_file(
http-time-buffer = "{}ms"
fee-handler = {}
merge-solutions = {}
margin-bps = {}
"#,
solver.name,
addr,
Expand All @@ -333,6 +334,7 @@ async fn create_config_file(
solver.timeouts.http_delay.num_milliseconds(),
serde_json::to_string(&solver.fee_handler).unwrap(),
solver.merge_solutions,
solver.margin_bps,
)
.unwrap();
}
Expand Down
9 changes: 9 additions & 0 deletions crates/driver/src/tests/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ pub struct Solver {
/// Whether or not solver is allowed to combine multiple solutions into a
/// new one.
merge_solutions: bool,
/// Margin in basis points (0-10000) for conservative bidding.
margin_bps: u32,
}

#[derive(Debug, Clone)]
Expand All @@ -386,6 +388,7 @@ pub fn test_solver() -> Solver {
},
fee_handler: FeeHandler::default(),
merge_solutions: false,
margin_bps: 0,
}
}

Expand Down Expand Up @@ -424,6 +427,11 @@ impl Solver {
self.merge_solutions = true;
self
}

pub fn margin_bps(mut self, margin_bps: u32) -> Self {
self.margin_bps = margin_bps;
self
}
}

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -963,6 +971,7 @@ impl Setup {
expected_surplus_capturing_jit_order_owners: surplus_capturing_jit_order_owners
.clone(),
allow_multiple_solve_requests: self.allow_multiple_solve_requests,
margin_bps: solver.margin_bps,
})
.await;

Expand Down
18 changes: 18 additions & 0 deletions crates/driver/src/tests/setup/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub struct Config<'a> {
pub private_key: PrivateKeySigner,
pub expected_surplus_capturing_jit_order_owners: Vec<Address>,
pub allow_multiple_solve_requests: bool,
/// Margin in basis points (0-10000) for conservative bidding.
pub margin_bps: u32,
}

impl Solver {
Expand Down Expand Up @@ -89,6 +91,14 @@ impl Solver {
_ => {}
}
}
// Apply margin: reduce sell amount for buy orders
if config.margin_bps > 0 {
let factor = config.margin_bps as f64 / 10_000.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the issue identified in dto/auction.rs, using f64 for factor calculation in the mock solver can introduce precision problems. For consistency and accuracy in financial simulations, it's best to use integer or BigRational arithmetic.

Suggested change
let factor = config.margin_bps as f64 / 10_000.0;
let factor = eth::U256::from(config.margin_bps);
let denominator = eth::U256::from(10_000);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

current_sell_amount = eth::TokenAmount(current_sell_amount)
.apply_factor(1.0 / (1.0 + factor))
.unwrap()
.0;
}
current_sell_amount.to_string()
}
_ => quote.sell_amount().to_string(),
Expand All @@ -115,6 +125,14 @@ impl Solver {
_ => {}
}
}
// Apply margin: increase buy amount for sell orders
if config.margin_bps > 0 {
let factor = config.margin_bps as f64 / 10_000.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Again, using f64 for factor calculation in the mock solver can lead to precision issues. It's recommended to use integer or BigRational arithmetic for financial calculations.

Suggested change
let factor = config.margin_bps as f64 / 10_000.0;
let factor = eth::U256::from(config.margin_bps);
let denominator = eth::U256::from(10_000);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

current_buy_amount = eth::TokenAmount(current_buy_amount)
.apply_factor(1.0 / (1.0 - factor))
.unwrap()
.0;
}
current_buy_amount.to_string()
}
_ => quote.buy_amount().to_string(),
Expand Down
25 changes: 25 additions & 0 deletions crates/e2e/src/setup/colocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct SolverEngine {
pub account: TestAccount,
pub base_tokens: Vec<Address>,
pub merge_solutions: bool,
pub margin_bps: u32,
}

pub async fn start_baseline_solver(
Expand All @@ -23,6 +24,27 @@ pub async fn start_baseline_solver(
base_tokens: Vec<Address>,
max_hops: usize,
merge_solutions: bool,
) -> SolverEngine {
start_baseline_solver_with_margin(
name,
account,
weth,
base_tokens,
max_hops,
merge_solutions,
0,
)
.await
}

pub async fn start_baseline_solver_with_margin(
name: String,
account: TestAccount,
weth: Address,
base_tokens: Vec<Address>,
max_hops: usize,
merge_solutions: bool,
margin_bps: u32,
) -> SolverEngine {
let encoded_base_tokens = encode_base_tokens(base_tokens.clone());
let config_file = config_tmp_file(format!(
Expand All @@ -43,6 +65,7 @@ uni-v3-node-url = "http://localhost:8545"
account,
base_tokens,
merge_solutions,
margin_bps,
}
}

Expand Down Expand Up @@ -140,6 +163,7 @@ pub fn start_driver_with_config_override(
endpoint,
base_tokens: _,
merge_solutions,
margin_bps,
}| {
let account = account.signer.to_bytes();
format!(
Expand All @@ -155,6 +179,7 @@ enable-simulation-bad-token-detection = true
enable-metrics-bad-token-detection = true
http-time-buffer = "100ms"
solving-share-of-deadline = 1.0
margin-bps = {margin_bps}
"#
)
},
Expand Down
1 change: 1 addition & 0 deletions crates/e2e/src/setup/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ impl<'a> Services<'a> {
endpoint: external_solver_endpoint,
base_tokens: vec![],
merge_solutions: true,
margin_bps: 0,
}];

let (autopilot_args, api_args) = if run_baseline {
Expand Down
3 changes: 3 additions & 0 deletions crates/e2e/tests/e2e/cow_amm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ async fn cow_amm_jit(web3: Web3) {
endpoint: mock_solver.url.clone(),
base_tokens: vec![],
merge_solutions: true,
margin_bps: 0,
},
],
colocation::LiquidityProvider::UniswapV2,
Expand Down Expand Up @@ -534,6 +535,7 @@ async fn cow_amm_driver_support(web3: Web3) {
endpoint: mock_solver.url.clone(),
base_tokens: vec![],
merge_solutions: true,
margin_bps: 0,
},
],
colocation::LiquidityProvider::UniswapV2,
Expand Down Expand Up @@ -801,6 +803,7 @@ async fn cow_amm_opposite_direction(web3: Web3) {
endpoint: mock_solver.url.clone(),
base_tokens: vec![],
merge_solutions: true,
margin_bps: 0,
},
],
colocation::LiquidityProvider::UniswapV2,
Expand Down
1 change: 1 addition & 0 deletions crates/e2e/tests/e2e/jit_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ async fn single_limit_order_test(web3: Web3) {
endpoint: mock_solver.url.clone(),
base_tokens: vec![*token.address()],
merge_solutions: true,
margin_bps: 0,
},
],
colocation::LiquidityProvider::UniswapV2,
Expand Down
Loading
Loading