diff --git a/crates/balancer-solver/config/example.baseline.toml b/crates/balancer-solver/config/example.baseline.toml index 28d376008f..a69a55ceb0 100644 --- a/crates/balancer-solver/config/example.baseline.toml +++ b/crates/balancer-solver/config/example.baseline.toml @@ -13,3 +13,6 @@ native-token-price-estimation-amount = "100000000000000000" # driver-url = "http://localhost:8080" # timeout-ms = 5000 # protocols = ["balancer_v2", "uniswap_v2"] + +# Optional: Directory to save auction and solution JSON files for debugging +# auction-save-directory = "/tmp/balancer-auctions" diff --git a/crates/balancer-solver/config/example.legacy.toml b/crates/balancer-solver/config/example.legacy.toml index 5a02a7a047..329a21503e 100644 --- a/crates/balancer-solver/config/example.legacy.toml +++ b/crates/balancer-solver/config/example.legacy.toml @@ -2,3 +2,6 @@ chain-id = "1" solver-name = "CoW Solver" endpoint = "https://solver.cow.fi" gzip-requests = false + +# Optional: Directory to save auction and solution JSON files for debugging +# auction-save-directory = "/tmp/balancer-auctions" diff --git a/crates/balancer-solver/openapi.yml b/crates/balancer-solver/openapi.yml index 24025a7123..045e3226e8 100644 --- a/crates/balancer-solver/openapi.yml +++ b/crates/balancer-solver/openapi.yml @@ -518,7 +518,6 @@ components: - kind - tokens - fee - - balancer_pool_id properties: kind: type: string @@ -557,7 +556,6 @@ components: - tokens - amplificationParameter - fee - - balancer_pool_id properties: kind: type: string diff --git a/crates/balancer-solver/src/api/routes/solve/dto/auction.rs b/crates/balancer-solver/src/api/routes/solve/dto/auction.rs index 5bee9a4ee6..0feec5e5bf 100644 --- a/crates/balancer-solver/src/api/routes/solve/dto/auction.rs +++ b/crates/balancer-solver/src/api/routes/solve/dto/auction.rs @@ -79,13 +79,23 @@ fn extract_token_pairs_from_auction( /// Converts a data transfer object into its domain object representation. /// If liquidity_client is provided and auction has empty liquidity, fetches /// independently. +/// Returns the auction and optionally the fetched liquidity response. pub async fn into_domain( auction: Auction, liquidity_client: Option<&LiquidityClient>, base_tokens: Option<&[eth::H160]>, protocols: Option<&[String]>, -) -> Result { - Ok(auction::Auction { + save_directory: Option<&std::path::Path>, +) -> Result< + ( + auction::Auction, + Option, + ), + Error, +> { + let mut fetched_liquidity_response = None; + + let auction_domain = auction::Auction { id: match auction.id { Some(id) => auction::Id::Solve(id), None => auction::Id::Quote, @@ -188,12 +198,29 @@ pub async fn into_domain( "Successfully fetched liquidity from API" ); + // Save liquidity to JSON if save_directory is provided + if let Some(save_dir) = save_directory { + let liquidity_json = serde_json::to_value(&response).ok(); + let save_dir = save_dir.to_path_buf(); + let auction_id = auction.id; + tokio::spawn(async move { + if let Some(liquidity) = liquidity_json { + save_liquidity_json(liquidity, auction_id, &save_dir).await; + } + }); + } + // Process the fetched liquidity - response + let domain_liquidity = response .liquidity .iter() .map(|liquidity| convert_dto_liquidity_to_domain(liquidity)) - .try_collect()? + .try_collect()?; + + // Store the response for enhanced solutions + fetched_liquidity_response = Some(response); + + domain_liquidity } Err(e) => { tracing::warn!( @@ -215,7 +242,9 @@ pub async fn into_domain( }, gas_price: auction::GasPrice(eth::Ether(auction.effective_gas_price)), deadline: auction::Deadline(auction.deadline), - }) + }; + + Ok((auction_domain, fetched_liquidity_response)) } /// Helper function to convert DTO liquidity to domain liquidity @@ -238,6 +267,73 @@ fn convert_dto_liquidity_to_domain(liquidity: &Liquidity) -> Result, + save_dir: &std::path::Path, +) { + use tokio::fs; + + // Determine filename based on auction ID + let base_filename = match auction_id { + Some(id) => id.to_string(), + None => { + // Use timestamp for quote auctions + let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S_%3f"); + format!("quote_{}", timestamp) + } + }; + + let liquidity_file_path = save_dir.join(format!("{}_liquidity.json", base_filename)); + + // Create directory if it doesn't exist + if let Err(err) = fs::create_dir_all(save_dir).await { + tracing::warn!( + ?err, + directory = ?save_dir, + "Failed to create liquidity save directory" + ); + return; + } + + // Serialize liquidity to pretty JSON + let liquidity_json = match serde_json::to_string_pretty(&liquidity) { + Ok(content) => content, + Err(err) => { + tracing::warn!(?err, "Failed to serialize liquidity to JSON"); + return; + } + }; + + let liquidity_count = liquidity + .get("liquidity") + .and_then(|l| l.as_array()) + .map(|a| a.len()) + .unwrap_or(0); + + // Write liquidity file + match fs::write(&liquidity_file_path, liquidity_json).await { + Ok(_) => { + tracing::info!( + liquidity_file = ?liquidity_file_path, + auction_id = ?auction_id, + liquidity_count, + "💾 Saved fetched liquidity to JSON file" + ); + } + Err(err) => { + tracing::warn!( + ?err, + file_path = ?liquidity_file_path, + "Failed to write liquidity JSON file" + ); + } + } +} + mod erc4626 { use super::*; pub fn to_domain(edge: &Erc4626Edge) -> Result { @@ -301,6 +397,7 @@ mod weighted_product_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -340,6 +437,7 @@ mod stable_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -439,6 +537,7 @@ mod gyro_e_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -502,6 +601,7 @@ mod gyro_2clp_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -547,6 +647,7 @@ mod gyro_3clp_pool { .ok_or("invalid scaling factor")?, ) .ok_or("invalid scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -585,6 +686,7 @@ mod reclamm_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -644,6 +746,7 @@ mod stable_surge_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -685,6 +788,7 @@ mod quant_amm_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -725,3 +829,60 @@ mod quant_amm_pool { }) } } + +/// Creates an enhanced solutions JSON with full liquidity details embedded +pub fn create_enhanced_solutions( + solutions: &solvers_dto::solution::Solutions, + liquidity_response: &crate::infra::liquidity_client::LiquidityResponse, +) -> serde_json::Value { + // Convert to JSON value + let mut solutions_json = serde_json::to_value(solutions).unwrap(); + + // Build a map of liquidity ID -> full liquidity details + let mut liquidity_map: std::collections::HashMap = + std::collections::HashMap::new(); + + for liq in &liquidity_response.liquidity { + let id = extract_liquidity_id(liq); + liquidity_map.insert(id, liq); + } + + // Enhance each solution's interactions + if let Some(solutions_array) = solutions_json["solutions"].as_array_mut() { + for solution in solutions_array { + if let Some(interactions) = solution["interactions"].as_array_mut() { + for interaction in interactions { + if interaction["kind"] == "liquidity" { + if let Some(id) = interaction["id"].as_str() { + if let Some(liquidity_details) = liquidity_map.get(id) { + // Embed full liquidity details + interaction["liquidityDetails"] = + serde_json::to_value(liquidity_details).unwrap(); + } + } + } + } + } + } + } + + solutions_json +} + +fn extract_liquidity_id(liq: &solvers_dto::auction::Liquidity) -> String { + // Extract ID from each liquidity variant + match liq { + solvers_dto::auction::Liquidity::ConstantProduct(p) => p.id.clone(), + solvers_dto::auction::Liquidity::WeightedProduct(p) => p.id.clone(), + solvers_dto::auction::Liquidity::Stable(p) => p.id.clone(), + solvers_dto::auction::Liquidity::ConcentratedLiquidity(p) => p.id.clone(), + solvers_dto::auction::Liquidity::GyroE(p) => p.id.clone(), + solvers_dto::auction::Liquidity::Gyro2CLP(p) => p.id.clone(), + solvers_dto::auction::Liquidity::Gyro3CLP(p) => p.id.clone(), + solvers_dto::auction::Liquidity::LimitOrder(p) => p.id.clone(), + solvers_dto::auction::Liquidity::Erc4626(p) => p.id.clone(), + solvers_dto::auction::Liquidity::ReClamm(p) => p.id.clone(), + solvers_dto::auction::Liquidity::QuantAmm(p) => p.id.clone(), + solvers_dto::auction::Liquidity::StableSurge(p) => p.id.clone(), + } +} diff --git a/crates/balancer-solver/src/api/routes/solve/mod.rs b/crates/balancer-solver/src/api/routes/solve/mod.rs index d94ba423b5..a2c5a39ba7 100644 --- a/crates/balancer-solver/src/api/routes/solve/mod.rs +++ b/crates/balancer-solver/src/api/routes/solve/mod.rs @@ -79,11 +79,15 @@ pub async fn solve( }; let protocols = state.protocols(); - let auction = match dto::auction::into_domain( + // Serialize auction DTO for potential saving later (before consuming it) + let auction_json = serde_json::to_value(&auction).ok(); + + let (auction, fetched_liquidity) = match dto::auction::into_domain( auction, liquidity_client, base_tokens.as_deref(), protocols.as_deref(), + state.auction_save_directory(), ) .await { @@ -128,6 +132,86 @@ pub async fn solve( "✅ SENDING RESPONSE TO COW PROTOCOL" ); + // Save auction and solutions to JSON if configured (non-blocking) + if let (Some(save_dir), Some(auction_json)) = (state.auction_save_directory(), auction_json) + { + let solutions_json = serde_json::to_value(&solutions_dto).ok(); + let save_dir = save_dir.to_path_buf(); + let save_dir_for_competition = save_dir.clone(); + let save_dir_for_enhanced = save_dir.clone(); + let save_dir_for_verify = save_dir.clone(); + + tokio::spawn(async move { + if let Some(solutions) = solutions_json { + save_auction_and_solutions(auction_json, solutions, &save_dir).await; + } + }); + + // Spawn background task to fetch competition data + let cow_api_url = state.cow_api_base_url(); + tokio::spawn(async move { + fetch_and_save_competition_data(auction_id, cow_api_url, &save_dir_for_competition) + .await; + }); + + // Spawn background task to create enhanced solutions if liquidity was fetched + // If verifier is also configured, verify using the enhanced solutions + if let Some(liq_response) = fetched_liquidity { + let verifier_opt = state.verifier().cloned(); + let solutions_json_for_enhanced = serde_json::to_value(&solutions_dto).ok(); + + tokio::spawn(async move { + if let Some(solutions_json) = solutions_json_for_enhanced { + // Deserialize back to Solutions for the function + if let Ok(solutions_for_enhance) = + serde_json::from_value::(solutions_json) + { + // Create enhanced solutions with liquidityDetails + let enhanced = dto::auction::create_enhanced_solutions( + &solutions_for_enhance, + &liq_response, + ); + + // Save enhanced solutions file + save_enhanced_solutions_json( + enhanced.clone(), + auction_id, + &save_dir_for_enhanced, + ) + .await; + + // Verify using enhanced solutions if verifier is configured + if let Some(verifier) = verifier_opt { + verify_and_save_solutions( + enhanced, + verifier, + auction_id, + &save_dir_for_verify, + ) + .await; + } + } + } + }); + } else if let Some(verifier) = state.verifier() { + // No liquidity fetched, but verifier configured - use basic solutions + let solutions_json_for_verify = serde_json::to_value(&solutions_dto).ok(); + let verifier = verifier.clone(); + + tokio::spawn(async move { + if let Some(solutions_json) = solutions_json_for_verify { + verify_and_save_solutions( + solutions_json, + verifier, + auction_id, + &save_dir_for_verify, + ) + .await; + } + }); + } + } + ( axum::http::StatusCode::OK, axum::response::Json(Response::Ok(solutions_dto)), @@ -138,3 +222,369 @@ pub async fn solve( .instrument(tracing::info_span!("/solve")) .await } + +/// Saves auction and solutions to separate JSON files in the configured +/// directory. This function runs in a background task and logs errors without +/// failing the request. +async fn save_auction_and_solutions( + auction: serde_json::Value, + solutions: serde_json::Value, + save_dir: &std::path::Path, +) { + use tokio::fs; + + // Determine base filename based on auction ID + let base_filename = match auction.get("id").and_then(|v| v.as_str()) { + Some(id) => id.to_string(), + None => { + // Use timestamp for quote auctions + let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S_%3f"); + format!("quote_{}", timestamp) + } + }; + + let auction_file_path = save_dir.join(format!("{}_auction.json", base_filename)); + let solutions_file_path = save_dir.join(format!("{}_solutions.json", base_filename)); + + // Create directory if it doesn't exist + if let Err(err) = fs::create_dir_all(save_dir).await { + tracing::warn!( + ?err, + directory = ?save_dir, + "Failed to create auction save directory" + ); + return; + } + + // Serialize auction to pretty JSON + let auction_json = match serde_json::to_string_pretty(&auction) { + Ok(content) => content, + Err(err) => { + tracing::warn!(?err, "Failed to serialize auction to JSON"); + return; + } + }; + + // Serialize solutions to pretty JSON + let solutions_json = match serde_json::to_string_pretty(&solutions) { + Ok(content) => content, + Err(err) => { + tracing::warn!(?err, "Failed to serialize solutions to JSON"); + return; + } + }; + + let solutions_count = solutions + .get("solutions") + .and_then(|s| s.as_array()) + .map(|a| a.len()) + .unwrap_or(0); + + // Write auction file + let auction_write_result = fs::write(&auction_file_path, auction_json).await; + + // Write solutions file + let solutions_write_result = fs::write(&solutions_file_path, solutions_json).await; + + // Log results + match (auction_write_result, solutions_write_result) { + (Ok(_), Ok(_)) => { + tracing::info!( + auction_file = ?auction_file_path, + solutions_file = ?solutions_file_path, + auction_id = ?auction.get("id"), + solutions_count, + "💾 Saved auction and solutions to separate JSON files" + ); + } + (Err(err), _) => { + tracing::warn!( + ?err, + file_path = ?auction_file_path, + "Failed to write auction JSON file" + ); + } + (_, Err(err)) => { + tracing::warn!( + ?err, + file_path = ?solutions_file_path, + "Failed to write solutions JSON file" + ); + } + } +} + +/// Fetches competition data from the CoW API and saves it to a JSON file. +/// This function waits 60 seconds before attempting to fetch, then retries up +/// to 10 times. +async fn fetch_and_save_competition_data( + auction_id: crate::domain::auction::Id, + cow_api_base_url: &str, + save_dir: &std::path::Path, +) { + use tokio::{ + fs, + time::{Duration, sleep}, + }; + + // Extract the numeric auction ID + let auction_id_num = match auction_id { + crate::domain::auction::Id::Solve(id) => id, + crate::domain::auction::Id::Quote => { + tracing::debug!("Skipping competition data fetch for quote auction"); + return; + } + }; + + // Wait 60 seconds for the competition to settle + tracing::info!( + auction_id = auction_id_num, + "Waiting 60 seconds before fetching competition data" + ); + sleep(Duration::from_secs(60)).await; + + let url = format!( + "{}/api/v2/solver_competition/{}", + cow_api_base_url, auction_id_num + ); + let client = reqwest::Client::new(); + + // Retry up to 10 times with 10 second delays between attempts + for attempt in 1..=10 { + tracing::debug!( + auction_id = auction_id_num, + attempt, + "Fetching competition data" + ); + + match client.get(&url).send().await { + Ok(response) => { + if response.status().is_success() { + match response.json::().await { + Ok(competition_data) => { + // Save to file + let filename = format!("{}_competition.json", auction_id_num); + let file_path = save_dir.join(filename); + + // Create directory if needed + if let Err(err) = fs::create_dir_all(save_dir).await { + tracing::warn!( + ?err, + directory = ?save_dir, + "Failed to create competition data directory" + ); + return; + } + + // Serialize to pretty JSON + let json_string = match serde_json::to_string_pretty(&competition_data) + { + Ok(s) => s, + Err(err) => { + tracing::warn!(?err, "Failed to serialize competition data"); + return; + } + }; + + // Write to file + match fs::write(&file_path, json_string).await { + Ok(_) => { + tracing::info!( + auction_id = auction_id_num, + file_path = ?file_path, + attempt, + "💾 Successfully saved competition data" + ); + return; // Success! + } + Err(err) => { + tracing::warn!( + ?err, + file_path = ?file_path, + "Failed to write competition data file" + ); + return; + } + } + } + Err(err) => { + tracing::warn!( + ?err, + auction_id = auction_id_num, + attempt, + "Failed to parse competition data JSON" + ); + } + } + } else if response.status().as_u16() == 404 { + tracing::debug!( + auction_id = auction_id_num, + attempt, + "Competition data not yet available (404), will retry" + ); + } else { + tracing::warn!( + auction_id = auction_id_num, + status = response.status().as_u16(), + attempt, + "Unexpected HTTP status when fetching competition data" + ); + } + } + Err(err) => { + tracing::warn!( + ?err, + auction_id = auction_id_num, + attempt, + "HTTP request failed when fetching competition data" + ); + } + } + + // Wait 10 seconds before next retry (unless this was the last attempt) + if attempt < 10 { + sleep(Duration::from_secs(10)).await; + } + } + + tracing::warn!( + auction_id = auction_id_num, + "Failed to fetch competition data after 10 attempts" + ); +} + +/// Verifies solutions against on-chain Balancer contracts and saves results +/// Accepts JSON solutions (possibly enhanced with liquidityDetails) +async fn verify_and_save_solutions( + solutions_json: serde_json::Value, + verifier: crate::infra::solution_verifier::SolutionVerifier, + auction_id: crate::domain::auction::Id, + save_dir: &std::path::Path, +) { + use tokio::fs; + + let auction_id_num = match auction_id { + crate::domain::auction::Id::Solve(id) => id, + crate::domain::auction::Id::Quote => { + tracing::debug!("Skipping verification for quote auction"); + return; + } + }; + + // Extract solutions array from JSON + let solutions_array = match solutions_json["solutions"].as_array() { + Some(arr) => arr, + None => { + tracing::warn!("Solutions JSON missing 'solutions' array"); + return; + } + }; + + tracing::info!( + auction_id = auction_id_num, + solutions_count = solutions_array.len(), + has_liquidity_details = solutions_array + .get(0) + .and_then(|s| s["interactions"].as_array()) + .and_then(|i| i.get(0)) + .and_then(|i| i.get("liquidityDetails")) + .is_some(), + "Starting solution verification with enhanced liquidity data" + ); + + // Verify each solution in parallel + let mut verification_futures = Vec::new(); + for (idx, solution) in solutions_array.iter().enumerate() { + let verifier_clone = verifier.clone(); + let solution = solution.clone(); + verification_futures.push(tokio::spawn(async move { + verifier_clone.verify_solution(&solution, idx).await + })); + } + + let results: Vec<_> = futures::future::join_all(verification_futures) + .await + .into_iter() + .filter_map(|r| r.ok()) + .collect(); + + // Save results + let filename = format!("{}_solution_verification.json", auction_id_num); + let file_path = save_dir.join(filename); + + if let Err(err) = fs::create_dir_all(save_dir).await { + tracing::warn!(?err, "Failed to create verification directory"); + return; + } + + let json_string = match serde_json::to_string_pretty(&results) { + Ok(s) => s, + Err(err) => { + tracing::warn!(?err, "Failed to serialize verification results"); + return; + } + }; + + match fs::write(&file_path, json_string).await { + Ok(_) => { + tracing::info!( + auction_id = auction_id_num, + file_path = ?file_path, + solutions_verified = results.len(), + "💾 Saved solution verification results" + ); + } + Err(err) => { + tracing::warn!(?err, "Failed to write verification file"); + } + } +} + +/// Saves enhanced solutions (already created) to a JSON file +async fn save_enhanced_solutions_json( + enhanced: serde_json::Value, + auction_id: crate::domain::auction::Id, + save_dir: &std::path::Path, +) { + use tokio::fs; + + let auction_id_num = match auction_id { + crate::domain::auction::Id::Solve(id) => id, + crate::domain::auction::Id::Quote => { + tracing::debug!("Skipping enhanced solutions for quote auction"); + return; + } + }; + + let filename = format!("{}_enhanced_solutions.json", auction_id_num); + let file_path = save_dir.join(filename); + + // Create directory if needed + if let Err(err) = fs::create_dir_all(save_dir).await { + tracing::warn!(?err, directory = ?save_dir, "Failed to create directory"); + return; + } + + // Serialize to pretty JSON + let json_string = match serde_json::to_string_pretty(&enhanced) { + Ok(s) => s, + Err(err) => { + tracing::warn!(?err, "Failed to serialize enhanced solutions"); + return; + } + }; + + // Write to file + match fs::write(&file_path, json_string).await { + Ok(_) => { + tracing::info!( + auction_id = auction_id_num, + file_path = ?file_path, + "💾 Saved enhanced solutions with liquidity details" + ); + } + Err(err) => { + tracing::warn!(?err, file_path = ?file_path, "Failed to write enhanced solutions"); + } + } +} diff --git a/crates/balancer-solver/src/boundary/liquidity/gyro_2clp.rs b/crates/balancer-solver/src/boundary/liquidity/gyro_2clp.rs index 81c9c99f6a..15934893c5 100644 --- a/crates/balancer-solver/src/boundary/liquidity/gyro_2clp.rs +++ b/crates/balancer-solver/src/boundary/liquidity/gyro_2clp.rs @@ -26,7 +26,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::gyro_2clp::Pool) -> Opt TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -66,3 +66,10 @@ fn to_signed_fixed_point(ratio: ð::SignedRational) -> Option { let scaled = ratio.numer().checked_mul(base)? / *ratio.denom(); Some(SBfp::from_wei(scaled)) } + +/// Converts a rational to a U256 rate in wei (18 decimals). +/// Rates are stored as Rationals and need to be scaled to 18 decimals. +fn to_u256(ratio: ð::Rational) -> Option { + let base = U256::exp10(18); + ratio.numer().checked_mul(base)?.checked_div(*ratio.denom()) +} diff --git a/crates/balancer-solver/src/boundary/liquidity/gyro_3clp.rs b/crates/balancer-solver/src/boundary/liquidity/gyro_3clp.rs index 8a306c6c68..4a7877a2d7 100644 --- a/crates/balancer-solver/src/boundary/liquidity/gyro_3clp.rs +++ b/crates/balancer-solver/src/boundary/liquidity/gyro_3clp.rs @@ -31,7 +31,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::gyro_3clp::Pool) -> Opt TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -62,3 +62,10 @@ fn to_fixed_point(ratio: &crate::domain::eth::Rational) -> Option { let wei = ratio.numer().checked_mul(base)? / ratio.denom(); Some(Bfp::from_wei(wei)) } + +/// Converts a rational to a U256 rate in wei (18 decimals). +/// Rates are stored as Rationals and need to be scaled to 18 decimals. +fn to_u256(ratio: &crate::domain::eth::Rational) -> Option { + let base = ethereum_types::U256::exp10(18); + ratio.numer().checked_mul(base)?.checked_div(*ratio.denom()) +} diff --git a/crates/balancer-solver/src/boundary/liquidity/gyro_e.rs b/crates/balancer-solver/src/boundary/liquidity/gyro_e.rs index dd6537a2ca..f2562d21dc 100644 --- a/crates/balancer-solver/src/boundary/liquidity/gyro_e.rs +++ b/crates/balancer-solver/src/boundary/liquidity/gyro_e.rs @@ -31,7 +31,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::gyro_e::Pool) -> Option TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -76,6 +76,13 @@ fn to_fixed_point(ratio: ð::Rational) -> Option { Some(Bfp::from_wei(wei)) } +/// Converts a rational to a U256 rate in wei (18 decimals). +/// Rates are stored as Rationals and need to be scaled to 18 decimals. +fn to_u256(ratio: ð::Rational) -> Option { + let base = U256::exp10(18); + ratio.numer().checked_mul(base)?.checked_div(*ratio.denom()) +} + /// Converts a signed rational to a Balancer signed fixed point number. fn to_signed_fixed_point(ratio: ð::SignedRational) -> Option { // For SignedRational (based on I256), we can work directly with signed values diff --git a/crates/balancer-solver/src/boundary/liquidity/quantamm.rs b/crates/balancer-solver/src/boundary/liquidity/quantamm.rs index b2d4ccabe4..ca87b424cb 100644 --- a/crates/balancer-solver/src/boundary/liquidity/quantamm.rs +++ b/crates/balancer-solver/src/boundary/liquidity/quantamm.rs @@ -28,7 +28,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::quantamm::Pool) -> Opti TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -77,3 +77,10 @@ fn to_signed_i256(ratio: ð::SignedRational) -> Option { Some(wei_i256) } + +/// Converts a rational to a U256 rate in wei (18 decimals). +/// Rates are stored as Rationals and need to be scaled to 18 decimals. +fn to_u256(ratio: ð::Rational) -> Option { + let base = U256::exp10(18); + ratio.numer().checked_mul(base)?.checked_div(*ratio.denom()) +} diff --git a/crates/balancer-solver/src/boundary/liquidity/reclamm.rs b/crates/balancer-solver/src/boundary/liquidity/reclamm.rs index 245682181b..d3992cdee4 100644 --- a/crates/balancer-solver/src/boundary/liquidity/reclamm.rs +++ b/crates/balancer-solver/src/boundary/liquidity/reclamm.rs @@ -27,7 +27,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::reclamm::Pool) -> Optio TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -57,3 +57,10 @@ fn to_fixed_point(ratio: ð::Rational) -> Option { let wei = ratio.numer().checked_mul(base)? / ratio.denom(); Some(Bfp::from_wei(wei)) } + +/// Converts a rational to a U256 rate in wei (18 decimals). +/// Rates are stored as Rationals and need to be scaled to 18 decimals. +fn to_u256(ratio: ð::Rational) -> Option { + let base = U256::exp10(18); + ratio.numer().checked_mul(base)?.checked_div(*ratio.denom()) +} diff --git a/crates/balancer-solver/src/boundary/liquidity/stable.rs b/crates/balancer-solver/src/boundary/liquidity/stable.rs index f7f85954ac..cbb808e310 100644 --- a/crates/balancer-solver/src/boundary/liquidity/stable.rs +++ b/crates/balancer-solver/src/boundary/liquidity/stable.rs @@ -31,7 +31,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::stable::Pool) -> Option TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -63,3 +63,10 @@ fn to_fixed_point(ratio: ð::Rational) -> Option { let wei = ratio.numer().checked_mul(base)? / ratio.denom(); Some(Bfp::from_wei(wei)) } + +/// Converts a rational to a U256 rate in wei (18 decimals). +/// Rates are stored as Rationals and need to be scaled to 18 decimals. +fn to_u256(ratio: ð::Rational) -> Option { + let base = U256::exp10(18); + ratio.numer().checked_mul(base)?.checked_div(*ratio.denom()) +} diff --git a/crates/balancer-solver/src/boundary/liquidity/weighted_product.rs b/crates/balancer-solver/src/boundary/liquidity/weighted_product.rs index a4552c9c53..86bf43b0cf 100644 --- a/crates/balancer-solver/src/boundary/liquidity/weighted_product.rs +++ b/crates/balancer-solver/src/boundary/liquidity/weighted_product.rs @@ -32,7 +32,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::weighted_product::Pool) common: TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, weight: to_fixed_point(&reserve.weight)?, }, @@ -64,3 +64,10 @@ fn to_fixed_point(ratio: ð::Rational) -> Option { let wei = ratio.numer().checked_mul(base)? / ratio.denom(); Some(Bfp::from_wei(wei)) } + +/// Converts a rational to a U256 rate in wei (18 decimals). +/// Rates are stored as Rationals and need to be scaled to 18 decimals. +fn to_u256(ratio: ð::Rational) -> Option { + let base = U256::exp10(18); + ratio.numer().checked_mul(base)?.checked_div(*ratio.denom()) +} diff --git a/crates/balancer-solver/src/domain/liquidity/gyro_2clp.rs b/crates/balancer-solver/src/domain/liquidity/gyro_2clp.rs index 9244afd9c7..ee5590bb80 100644 --- a/crates/balancer-solver/src/domain/liquidity/gyro_2clp.rs +++ b/crates/balancer-solver/src/domain/liquidity/gyro_2clp.rs @@ -60,6 +60,7 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The Gyroscope 2-CLP pool version. diff --git a/crates/balancer-solver/src/domain/liquidity/gyro_3clp.rs b/crates/balancer-solver/src/domain/liquidity/gyro_3clp.rs index e20f728433..de73083f17 100644 --- a/crates/balancer-solver/src/domain/liquidity/gyro_3clp.rs +++ b/crates/balancer-solver/src/domain/liquidity/gyro_3clp.rs @@ -66,6 +66,7 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The Gyroscope 3-CLP pool version. diff --git a/crates/balancer-solver/src/domain/liquidity/gyro_e.rs b/crates/balancer-solver/src/domain/liquidity/gyro_e.rs index 0cf93772b2..35ef58d935 100644 --- a/crates/balancer-solver/src/domain/liquidity/gyro_e.rs +++ b/crates/balancer-solver/src/domain/liquidity/gyro_e.rs @@ -72,6 +72,7 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The Gyroscope E-CLP pool version. diff --git a/crates/balancer-solver/src/domain/liquidity/quantamm.rs b/crates/balancer-solver/src/domain/liquidity/quantamm.rs index 1e7033edf1..e0526f53bd 100644 --- a/crates/balancer-solver/src/domain/liquidity/quantamm.rs +++ b/crates/balancer-solver/src/domain/liquidity/quantamm.rs @@ -59,6 +59,7 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The QuantAMM pool version. diff --git a/crates/balancer-solver/src/domain/liquidity/reclamm.rs b/crates/balancer-solver/src/domain/liquidity/reclamm.rs index 5b024846bc..660e62bbd5 100644 --- a/crates/balancer-solver/src/domain/liquidity/reclamm.rs +++ b/crates/balancer-solver/src/domain/liquidity/reclamm.rs @@ -48,4 +48,5 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: super::ScalingFactor, + pub rate: eth::Rational, } diff --git a/crates/balancer-solver/src/domain/liquidity/stable.rs b/crates/balancer-solver/src/domain/liquidity/stable.rs index 81eeb6bd3d..3848034f9d 100644 --- a/crates/balancer-solver/src/domain/liquidity/stable.rs +++ b/crates/balancer-solver/src/domain/liquidity/stable.rs @@ -56,4 +56,5 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } diff --git a/crates/balancer-solver/src/domain/liquidity/weighted_product.rs b/crates/balancer-solver/src/domain/liquidity/weighted_product.rs index 730369b050..513869fcf1 100644 --- a/crates/balancer-solver/src/domain/liquidity/weighted_product.rs +++ b/crates/balancer-solver/src/domain/liquidity/weighted_product.rs @@ -57,6 +57,7 @@ pub struct Reserve { pub asset: eth::Asset, pub weight: eth::Rational, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The version of the weighted product math to use. Different versions have diff --git a/crates/balancer-solver/src/domain/solver.rs b/crates/balancer-solver/src/domain/solver.rs index 298333ddb4..b8954ba495 100644 --- a/crates/balancer-solver/src/domain/solver.rs +++ b/crates/balancer-solver/src/domain/solver.rs @@ -31,6 +31,7 @@ pub struct Solver(Arc); const DEADLINE_SLACK: chrono::Duration = chrono::Duration::milliseconds(500); pub struct Config { + pub chain_id: u64, pub weth: eth::WethAddress, pub base_tokens: Vec, pub max_hops: usize, @@ -40,9 +41,14 @@ pub struct Config { pub uni_v3_node_url: Option, pub erc4626_node_url: Option, pub liquidity_client_config: Option, + pub auction_save_directory: Option, + pub vault_address: Option, + pub batch_router_address: Option, + pub node_url: Option, } struct Inner { + chain_id: u64, weth: eth::WethAddress, /// Set of tokens to additionally consider as intermediary hops when @@ -80,6 +86,12 @@ struct Inner { /// Optional liquidity client for fetching liquidity from external API liquidity_client: Option, + + /// Optional directory to save auction and solution JSON files + auction_save_directory: Option, + + /// Optional solution verifier for on-chain quote verification + verifier: Option, } impl Solver { @@ -125,7 +137,27 @@ impl Solver { ) }); + // Create solution verifier if vault and batch router addresses are provided + let verifier = match ( + config.vault_address, + config.batch_router_address, + config.node_url, + ) { + (Some(vault_addr), Some(batch_router_addr), Some(ref node_url)) => { + let web3 = + ethrpc::web3(Default::default(), Default::default(), node_url, "verifier"); + let vault = contracts::BalancerV2Vault::at(&web3, vault_addr.0); + let batch_router = contracts::BalancerV3BatchRouter::at(&web3, batch_router_addr.0); + Some(crate::infra::solution_verifier::SolutionVerifier::new( + vault, + batch_router, + )) + } + _ => None, + }; + Self(Arc::new(Inner { + chain_id: config.chain_id, weth: config.weth, base_tokens: config.base_tokens.into_iter().collect(), max_hops: config.max_hops, @@ -135,6 +167,8 @@ impl Solver { uni_v3_quoter_v2, erc4626_web3, liquidity_client, + auction_save_directory: config.auction_save_directory, + verifier, })) } @@ -156,6 +190,37 @@ impl Solver { }) } + /// Returns the auction save directory if configured + pub fn auction_save_directory(&self) -> Option<&std::path::Path> { + self.0.auction_save_directory.as_deref() + } + + /// Returns the chain ID for this solver + pub fn chain_id(&self) -> u64 { + self.0.chain_id + } + + /// Returns the CoW API base URL for this chain + pub fn cow_api_base_url(&self) -> &'static str { + match self.0.chain_id { + 1 => "https://api.cow.fi/mainnet", + 100 => "https://api.cow.fi/xdai", + 42161 => "https://api.cow.fi/arbitrum_one", + 8453 => "https://api.cow.fi/base", + 43114 => "https://api.cow.fi/avalanche", + 137 => "https://api.cow.fi/polygon", + _ => { + tracing::warn!(chain_id = self.0.chain_id, "Unknown chain ID for CoW API"); + "https://api.cow.fi/mainnet" // Fallback + } + } + } + + /// Returns a reference to the solution verifier if configured + pub fn verifier(&self) -> Option<&crate::infra::solution_verifier::SolutionVerifier> { + self.0.verifier.as_ref() + } + /// Solves the specified auction, returning a vector of all possible /// solutions. pub async fn solve(&self, auction: auction::Auction) -> Vec { diff --git a/crates/balancer-solver/src/infra/config.rs b/crates/balancer-solver/src/infra/config.rs index 638bdeb260..8eef01e655 100644 --- a/crates/balancer-solver/src/infra/config.rs +++ b/crates/balancer-solver/src/infra/config.rs @@ -61,6 +61,18 @@ struct Config { /// Configuration for independent liquidity fetching liquidity: Option, + + /// Optional directory path to save auction and solution JSON files + auction_save_directory: Option, + + /// Balancer V2 Vault address for solution verification + vault_address: Option, + + /// Balancer V3 Batch Router address for solution verification + batch_router_address: Option, + + /// Node URL for solution verification + node_url: Option, } /// Configuration for the liquidity client @@ -111,6 +123,7 @@ pub async fn load(path: &Path) -> solver::Config { }; solver::Config { + chain_id: config.chain_id.map(|c| c as u64).unwrap_or(1), weth, base_tokens: config .base_tokens @@ -124,6 +137,10 @@ pub async fn load(path: &Path) -> solver::Config { uni_v3_node_url: config.uni_v3_node_url, erc4626_node_url: config.erc4626_node_url, liquidity_client_config: config.liquidity, + auction_save_directory: config.auction_save_directory.map(std::path::PathBuf::from), + vault_address: config.vault_address.map(eth::Address), + batch_router_address: config.batch_router_address.map(eth::Address), + node_url: config.node_url, } } diff --git a/crates/balancer-solver/src/infra/liquidity_client.rs b/crates/balancer-solver/src/infra/liquidity_client.rs index 0990eb5d56..43ed685a6d 100644 --- a/crates/balancer-solver/src/infra/liquidity_client.rs +++ b/crates/balancer-solver/src/infra/liquidity_client.rs @@ -71,7 +71,7 @@ pub struct LiquidityRequest { } /// Response from the liquidity-driver API -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct LiquidityResponse { pub auction_id: u64, pub liquidity: Vec, diff --git a/crates/balancer-solver/src/infra/mod.rs b/crates/balancer-solver/src/infra/mod.rs index 8612ed4b6d..0e97262500 100644 --- a/crates/balancer-solver/src/infra/mod.rs +++ b/crates/balancer-solver/src/infra/mod.rs @@ -3,3 +3,4 @@ pub mod config; pub mod contracts; pub mod liquidity_client; pub mod metrics; +pub mod solution_verifier; diff --git a/crates/balancer-solver/src/infra/solution_verifier.rs b/crates/balancer-solver/src/infra/solution_verifier.rs new file mode 100644 index 0000000000..50efca2dd5 --- /dev/null +++ b/crates/balancer-solver/src/infra/solution_verifier.rs @@ -0,0 +1,448 @@ +use { + contracts::{BalancerV2Vault, BalancerV3BatchRouter}, + ethcontract::{Account, Address, Bytes, H160, U256}, + serde::{Deserialize, Serialize}, +}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct VerificationResult { + pub solution_index: usize, + pub swaps: Vec, + pub total_gas_estimate: Option, + pub verification_timestamp: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SwapVerification { + pub interaction_index: usize, + pub pool_id: String, + pub pool_version: PoolVersion, + pub token_in: Address, + pub token_out: Address, + pub amount_in: String, + pub expected_amount_out: String, + pub quoted_amount_out: Option, + pub difference_bps: Option, + pub quote_error: Option, + pub contract_call: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ContractCallDetails { + pub contract_address: String, + pub contract_name: String, + pub function_name: String, + pub calldata: String, + pub decoded_params: serde_json::Value, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub enum PoolVersion { + V2, + V3, +} + +#[derive(Clone)] +pub struct SolutionVerifier { + vault: BalancerV2Vault, + batch_router: BalancerV3BatchRouter, +} + +impl SolutionVerifier { + pub fn new(vault: BalancerV2Vault, batch_router: BalancerV3BatchRouter) -> Self { + Self { + vault, + batch_router, + } + } + + /// Detect pool version by ID length + fn detect_pool_version(pool_id: &str) -> PoolVersion { + // V2 pool IDs are 66 chars (0x + 64 hex chars) + // V3 pool IDs are 42 chars (same as address: 0x + 40 hex chars) + if pool_id.len() > 42 { + PoolVersion::V2 + } else { + PoolVersion::V3 + } + } + + /// Verify a single solution (accepts JSON to support enhanced solutions) + pub async fn verify_solution( + &self, + solution: &serde_json::Value, + solution_index: usize, + ) -> VerificationResult { + let mut swaps = Vec::new(); + + if let Some(interactions) = solution["interactions"].as_array() { + for (idx, interaction) in interactions.iter().enumerate() { + if interaction["kind"] == "liquidity" { + let verification = self.verify_swap(interaction, idx).await; + swaps.push(verification); + } + } + } + + VerificationResult { + solution_index, + swaps, + total_gas_estimate: None, + verification_timestamp: chrono::Utc::now().timestamp() as u64, + } + } + + /// Verify a single swap interaction (accepts JSON to support enhanced + /// solutions) + async fn verify_swap( + &self, + interaction: &serde_json::Value, + interaction_index: usize, + ) -> SwapVerification { + // Extract basic fields + let pool_id = interaction["id"].as_str().unwrap_or("unknown"); + let input_token_str = interaction["inputToken"].as_str().unwrap_or(""); + let output_token_str = interaction["outputToken"].as_str().unwrap_or(""); + let input_amount_str = interaction["inputAmount"].as_str().unwrap_or("0"); + let output_amount_str = interaction["outputAmount"].as_str().unwrap_or("0"); + + // Parse token addresses + let input_token: Address = input_token_str.parse().unwrap_or_default(); + let output_token: Address = output_token_str.parse().unwrap_or_default(); + let input_amount = U256::from_dec_str(input_amount_str).unwrap_or_default(); + let output_amount = U256::from_dec_str(output_amount_str).unwrap_or_default(); + + // Try to extract liquidityDetails (enhanced solutions) + let pool_details = interaction.get("liquidityDetails"); + + // Extract pool address and Balancer pool ID from liquidityDetails if available + let (pool_address_opt, balancer_pool_id_opt) = if let Some(details) = pool_details { + ( + details["address"].as_str(), + details["balancerPoolId"].as_str(), + ) + } else { + (None, None) + }; + + // Determine pool version: + // - If balancerPoolId is None/null, it's a V3 pool (V3 pools don't have + // V2-style pool IDs) + // - If balancerPoolId exists, detect version by ID length + let pool_version = if balancer_pool_id_opt.is_none() { + PoolVersion::V3 + } else { + Self::detect_pool_version(balancer_pool_id_opt.unwrap_or(pool_id)) + }; + + // Quote using appropriate method with enhanced data + let quoted_amount = match pool_version { + PoolVersion::V2 => { + if let Some(pool_id_hex) = balancer_pool_id_opt { + self.quote_v2_swap( + pool_id_hex, + H160::from(input_token.0), + H160::from(output_token.0), + input_amount, + ) + .await + } else { + Err("Missing balancerPoolId for V2 pool in liquidityDetails".into()) + } + } + PoolVersion::V3 => { + if let Some(address) = pool_address_opt { + self.quote_v3_swap( + address, + H160::from(input_token.0), + H160::from(output_token.0), + input_amount, + ) + .await + } else { + Err("Missing pool address for V3 pool in liquidityDetails".into()) + } + } + }; + + let (quoted_amount_out, difference_bps, quote_error, contract_call) = match quoted_amount { + Ok((quote, call_details)) => { + let diff = calculate_difference_bps(&output_amount, "e); + (Some(quote), diff, None, Some(call_details)) + } + Err(e) => { + // For V3 calls, we still want to save the call details even on error + // so the user can see what was actually attempted + let error_call_details = if pool_version == PoolVersion::V3 { + if let Some(address) = pool_address_opt { + Some(create_v3_call_details( + &self.batch_router, + address, + &input_token, + &output_token, + input_amount, + )) + } else { + None + } + } else { + None + }; + (None, None, Some(e.to_string()), error_call_details) + } + }; + + SwapVerification { + interaction_index, + pool_id: pool_id.to_string(), + pool_version, + token_in: input_token, + token_out: output_token, + amount_in: input_amount.to_string(), + expected_amount_out: output_amount.to_string(), + quoted_amount_out, + difference_bps, + quote_error, + contract_call, + } + } + + /// Quote V2 swap via Vault.queryBatchSwap + async fn quote_v2_swap( + &self, + balancer_pool_id: &str, + input_token: H160, + output_token: H160, + input_amount: U256, + ) -> Result<(String, ContractCallDetails), Box> { + // Parse pool ID (it's a hex string starting with 0x) + let pool_id_bytes = if balancer_pool_id.starts_with("0x") { + hex::decode(&balancer_pool_id[2..])? + } else { + hex::decode(balancer_pool_id)? + }; + + if pool_id_bytes.len() != 32 { + return Err(format!("Invalid V2 pool ID length: {}", pool_id_bytes.len()).into()); + } + + let mut pool_id = [0u8; 32]; + pool_id.copy_from_slice(&pool_id_bytes); + + // Build assets array: [token_in, token_out] + let assets = vec![input_token, output_token]; + + // Create BatchSwapStep + let swap = self.vault.methods().query_batch_swap( + 0u8.into(), // SwapKind.GIVEN_IN + vec![( + Bytes(pool_id), + 0u64.into(), // assetInIndex + 1u64.into(), // assetOutIndex + input_amount, + Bytes(vec![]), // empty userData + )], + assets.clone(), + ( + H160::zero(), // sender (not needed for query) + false, // fromInternalBalance + H160::zero(), // recipient (not needed for query) + false, // toInternalBalance + ), + ); + + // Capture contract call details for debugging + let calldata = swap + .tx + .data + .clone() + .map(|d| format!("0x{}", hex::encode(d.0))) + .unwrap_or_else(|| "0x".to_string()); + + let decoded_params = serde_json::json!({ + "kind": "GIVEN_IN (0)", + "swaps": [{ + "poolId": balancer_pool_id, + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": input_amount.to_string(), + "userData": "0x" + }], + "assets": vec![ + format!("{:?}", assets[0]), + format!("{:?}", assets[1]) + ], + "funds": { + "sender": "0x0000000000000000000000000000000000000000", + "fromInternalBalance": false, + "recipient": "0x0000000000000000000000000000000000000000", + "toInternalBalance": false + } + }); + + let call_details = ContractCallDetails { + contract_address: format!("{:?}", self.vault.address()), + contract_name: "BalancerV2Vault".to_string(), + function_name: "queryBatchSwap".to_string(), + calldata, + decoded_params, + }; + + // Call the query (static call) + let deltas = swap.call().await?; + + // Parse output: assetDeltas[1] represents net token flow for output token + // In Balancer V2: + // - Positive delta = tokens going INTO vault (user sends) + // - Negative delta = tokens coming OUT of vault (user receives) + // For the output token in a swap, we expect a NEGATIVE delta + if deltas.len() < 2 { + return Err("Invalid deltas returned from queryBatchSwap".into()); + } + + let amount_out = if deltas[1].is_negative() { + // Negative means tokens out - negate to get positive amount + (-deltas[1]).into_raw() + } else { + // Positive means tokens in, which is wrong for output token + return Err("Expected negative output delta (tokens out of vault)".into()); + }; + + Ok((amount_out.to_string(), call_details)) + } + + /// Quote V3 swap via Batch Router.querySwapExactIn + async fn quote_v3_swap( + &self, + pool_address_str: &str, + input_token: H160, + output_token: H160, + input_amount: U256, + ) -> Result<(String, ContractCallDetails), Box> { + // Parse pool address from string + let pool_address: H160 = pool_address_str.parse()?; + + // Build SwapPathExactAmountIn + let path = ( + input_token, // tokenIn + vec![( + pool_address, // pool + output_token, // tokenOut + false, // isBuffer + )], + input_amount, // exactAmountIn + U256::zero(), // minAmountOut (no minimum for query) + ); + + // Call querySwapExactIn + // IMPORTANT: Must set .from() to make this a proper staticcall + let query = self.batch_router.methods().query_swap_exact_in( + vec![path.clone()], + self.batch_router.address(), // sender (required for pools with hooks) + Bytes(vec![]), // empty userData + ) + .from(Account::Local(H160::zero(), None)); // Set from address for the eth_call + + // Capture contract call details for debugging + let calldata = query + .tx + .data + .clone() + .map(|d| format!("0x{}", hex::encode(d.0))) + .unwrap_or_else(|| "0x".to_string()); + + let decoded_params = serde_json::json!({ + "paths": [{ + "tokenIn": format!("{:?}", input_token), + "steps": [{ + "pool": pool_address_str, + "tokenOut": format!("{:?}", output_token), + "isBuffer": false + }], + "exactAmountIn": input_amount.to_string(), + "minAmountOut": "0" + }], + "sender": format!("{:?}", self.batch_router.address()), + "userData": "0x", + "from": "0x0000000000000000000000000000000000000000" + }); + + let call_details = ContractCallDetails { + contract_address: format!("{:?}", self.batch_router.address()), + contract_name: "BalancerV3BatchRouter".to_string(), + function_name: "querySwapExactIn".to_string(), + calldata, + decoded_params, + }; + + let result = query.call().await; + + match result { + Ok((path_amounts_out, _tokens_out, _amounts_out)) => { + // Get the first path's output amount + if path_amounts_out.is_empty() { + return Err("No output amounts returned from querySwapExactIn".into()); + } + Ok((path_amounts_out[0].to_string(), call_details)) + } + Err(e) => { + // Return the error - call details will be saved separately in the JSON + Err(Box::new(e)) + } + } + } +} + +fn create_v3_call_details( + batch_router: &BalancerV3BatchRouter, + pool_address: &str, + input_token: &Address, + output_token: &Address, + input_amount: U256, +) -> ContractCallDetails { + let decoded_params = serde_json::json!({ + "paths": [{ + "tokenIn": format!("{:?}", H160::from(input_token.0)), + "steps": [{ + "pool": pool_address, + "tokenOut": format!("{:?}", H160::from(output_token.0)), + "isBuffer": false + }], + "exactAmountIn": input_amount.to_string(), + "minAmountOut": "0" + }], + "sender": format!("{:?}", batch_router.address()), + "userData": "0x" + }); + + ContractCallDetails { + contract_address: format!("{:?}", batch_router.address()), + contract_name: "BalancerV3BatchRouter".to_string(), + function_name: "querySwapExactIn".to_string(), + calldata: "(error - call details captured without execution)".to_string(), + decoded_params, + } +} + +fn calculate_difference_bps(expected: &U256, actual: &str) -> Option { + // Parse actual amount + let actual_u256 = U256::from_dec_str(actual).ok()?; + + // Calculate difference in basis points + // diff_bps = ((actual - expected) / expected) * 10000 + if *expected == U256::zero() { + return None; + } + + let diff = if actual_u256 > *expected { + let delta = actual_u256 - *expected; + let bps = (delta * 10000u64) / *expected; + bps.as_u64() as i64 + } else { + let delta = *expected - actual_u256; + let bps = (delta * 10000u64) / *expected; + -(bps.as_u64() as i64) + }; + + Some(diff) +} diff --git a/crates/balancer-solver/src/tests/cases/bal_liquidity.rs b/crates/balancer-solver/src/tests/cases/bal_liquidity.rs index 6e74eb830c..31db6fd511 100644 --- a/crates/balancer-solver/src/tests/cases/bal_liquidity.rs +++ b/crates/balancer-solver/src/tests/cases/bal_liquidity.rs @@ -79,11 +79,13 @@ async fn weighted() { "balance": "11260752191375725565253", "scalingFactor": "1", "weight": "0.5", + "rate": "1000000000000000000", }, "0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab": { "balance": "18764168403990393422000071", "scalingFactor": "1", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -207,11 +209,13 @@ async fn weighted_v3plus() { "balance": "5089632258314443812936111", "scalingFactor": "1", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "3043530764763263654069", "scalingFactor": "1", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -381,14 +385,17 @@ async fn stable() { "0x6b175474e89094c44da98b954eedeac495271d0f": { "balance": "505781036390938593206504", "scalingFactor": "1", + "rate": "1.0", }, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { "balance": "554894862074", "scalingFactor": "1000000000000", + "rate": "1.0", }, "0xdac17f958d2ee523a2206206994597c13d831ec7": { "balance": "1585576741011", "scalingFactor": "1000000000000", + "rate": "1.0", }, }, "fee": "0.0001", @@ -557,14 +564,17 @@ async fn composable_stable_v4() { "0x4b1e2c2762667331bc91648052f646d1b0d35984": { "balance": "126041615528606990697699", "scalingFactor": "1", + "rate": "1.0", }, "0x5c78d05b8ecf97507d1cf70646082c54faa4da95": { "balance": "2596148429267369423681023550322451", "scalingFactor": "1", + "rate": "1.0", }, "0xcb444e90d8198415266c6a2724b7900fb12fc56e": { "balance": "170162457652825667152980", "scalingFactor": "1", + "rate": "1.0", }, }, "fee": "0.0001", diff --git a/crates/balancer-solver/src/tests/cases/buy_order_rounding.rs b/crates/balancer-solver/src/tests/cases/buy_order_rounding.rs index b0d96c60a1..16c0a62048 100644 --- a/crates/balancer-solver/src/tests/cases/buy_order_rounding.rs +++ b/crates/balancer-solver/src/tests/cases/buy_order_rounding.rs @@ -214,12 +214,16 @@ async fn balancer_weighted() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "1963528800698237927834721", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "1152796145430714835825", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -370,12 +374,16 @@ async fn balancer_weighted_v3plus() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "18764168403990393422000071", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "11260752191375725565253", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -514,12 +522,16 @@ async fn distant_convergence() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "5089632258314443812936111", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "3043530764763263654069", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -658,12 +670,16 @@ async fn same_path() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "1963528800698237927834721", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "1152796145430714835825", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -852,14 +868,17 @@ async fn balancer_stable() { "0x4b1e2c2762667331bc91648052f646d1b0d35984": { "balance": "126041615528606990697699", "scalingFactor": "1", + "rate": "1.0", }, "0x5c78d05b8ecf97507d1cf70646082c54faa4da95": { "balance": "2596148429267369423681023550322451", "scalingFactor": "1", + "rate": "1.0", }, "0xcb444e90d8198415266c6a2724b7900fb12fc56e": { "balance": "170162457652825667152980", "scalingFactor": "1", + "rate": "1.0", }, }, "fee": "0.0001", diff --git a/crates/balancer-solver/src/tests/cases/limit_order_quoting.rs b/crates/balancer-solver/src/tests/cases/limit_order_quoting.rs index fec623e088..057bf36a98 100644 --- a/crates/balancer-solver/src/tests/cases/limit_order_quoting.rs +++ b/crates/balancer-solver/src/tests/cases/limit_order_quoting.rs @@ -71,12 +71,16 @@ async fn sell_order() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "5089632258314443812936111", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "3043530764763263654069", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -216,12 +220,16 @@ async fn buy_order() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "5089632258314443812936111", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "3043530764763263654069", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", diff --git a/crates/driver/src/boundary/liquidity/balancer/v2/gyro_2clp.rs b/crates/driver/src/boundary/liquidity/balancer/v2/gyro_2clp.rs index 09220f0a14..b99a91a39c 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v2/gyro_2clp.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v2/gyro_2clp.rs @@ -34,6 +34,7 @@ pub fn to_domain(id: liquidity::Id, pool: Gyro2CLPPoolOrder) -> Result>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v2/gyro_3clp.rs b/crates/driver/src/boundary/liquidity/balancer/v2/gyro_3clp.rs index 4f269a967c..141b40bae2 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v2/gyro_3clp.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v2/gyro_3clp.rs @@ -33,6 +33,7 @@ pub fn to_domain(id: liquidity::Id, pool: Gyro3CLPPoolOrder) -> Result>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v2/gyro_e.rs b/crates/driver/src/boundary/liquidity/balancer/v2/gyro_e.rs index 4b1b68ad71..f35c52cade 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v2/gyro_e.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v2/gyro_e.rs @@ -34,6 +34,7 @@ pub fn to_domain(id: liquidity::Id, pool: GyroEPoolOrder) -> Result>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v2/stable.rs b/crates/driver/src/boundary/liquidity/balancer/v2/stable.rs index 1ce175a390..6290245f39 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v2/stable.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v2/stable.rs @@ -33,6 +33,7 @@ pub fn to_domain(id: liquidity::Id, pool: StablePoolOrder) -> Result>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v2/weighted.rs b/crates/driver/src/boundary/liquidity/balancer/v2/weighted.rs index 5d86fe77db..729ab4e69e 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v2/weighted.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v2/weighted.rs @@ -37,6 +37,7 @@ pub fn to_domain(id: liquidity::Id, pool: WeightedProductOrder) -> Result>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v3/gyro_2clp.rs b/crates/driver/src/boundary/liquidity/balancer/v3/gyro_2clp.rs index 4dbc165ba4..b7028f9456 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v3/gyro_2clp.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v3/gyro_2clp.rs @@ -32,6 +32,7 @@ pub fn to_domain(id: liquidity::Id, pool: BalancerV3Gyro2CLPOrder) -> Result>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v3/gyro_e.rs b/crates/driver/src/boundary/liquidity/balancer/v3/gyro_e.rs index 1df51e170b..423c5a9839 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v3/gyro_e.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v3/gyro_e.rs @@ -33,6 +33,7 @@ pub fn to_domain(id: liquidity::Id, pool: BalancerV3GyroEOrder) -> Result>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v3/quantamm.rs b/crates/driver/src/boundary/liquidity/balancer/v3/quantamm.rs index bc09c9e9e8..9ce4b735be 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v3/quantamm.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v3/quantamm.rs @@ -33,6 +33,7 @@ pub fn to_domain(id: liquidity::Id, pool: BalancerV3QuantAmmOrder) -> Result>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v3/reclamm.rs b/crates/driver/src/boundary/liquidity/balancer/v3/reclamm.rs index 98877833eb..343be5d182 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v3/reclamm.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v3/reclamm.rs @@ -32,6 +32,7 @@ pub fn to_domain(id: liquidity::Id, pool: BalancerV3ReClammOrder) -> Result>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v3/stable.rs b/crates/driver/src/boundary/liquidity/balancer/v3/stable.rs index d99c2a092d..3ce15cb2af 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v3/stable.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v3/stable.rs @@ -38,6 +38,7 @@ pub fn to_domain( scale: balancer::v3::ScalingFactor::from_raw( reserve.scaling_factor.as_uint256(), )?, + rate: reserve.rate.into(), }) }) .collect::>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v3/stable_surge.rs b/crates/driver/src/boundary/liquidity/balancer/v3/stable_surge.rs index ce4cdcf120..79697e054e 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v3/stable_surge.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v3/stable_surge.rs @@ -40,6 +40,7 @@ pub fn to_domain( scale: balancer::v3::ScalingFactor::from_raw( reserve.scaling_factor.as_uint256(), )?, + rate: reserve.rate.into(), }) }) .collect::>()?, diff --git a/crates/driver/src/boundary/liquidity/balancer/v3/weighted.rs b/crates/driver/src/boundary/liquidity/balancer/v3/weighted.rs index 64574f0aad..9365689a8d 100644 --- a/crates/driver/src/boundary/liquidity/balancer/v3/weighted.rs +++ b/crates/driver/src/boundary/liquidity/balancer/v3/weighted.rs @@ -39,6 +39,7 @@ pub fn to_domain( scale: balancer::v3::ScalingFactor::from_raw( reserve.common.scaling_factor.as_uint256(), )?, + rate: reserve.common.rate.into(), }) }) .collect::>()?, diff --git a/crates/driver/src/domain/liquidity/balancer/v2/gyro_2clp.rs b/crates/driver/src/domain/liquidity/balancer/v2/gyro_2clp.rs index 2766cbb157..5cd80ad197 100644 --- a/crates/driver/src/domain/liquidity/balancer/v2/gyro_2clp.rs +++ b/crates/driver/src/domain/liquidity/balancer/v2/gyro_2clp.rs @@ -103,6 +103,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Signed fixed point number used for Gyroscope 2-CLP parameters. diff --git a/crates/driver/src/domain/liquidity/balancer/v2/gyro_3clp.rs b/crates/driver/src/domain/liquidity/balancer/v2/gyro_3clp.rs index 5f076922f1..6eab560102 100644 --- a/crates/driver/src/domain/liquidity/balancer/v2/gyro_3clp.rs +++ b/crates/driver/src/domain/liquidity/balancer/v2/gyro_3clp.rs @@ -108,6 +108,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Fixed point number used for Gyroscope 3-CLP parameters. diff --git a/crates/driver/src/domain/liquidity/balancer/v2/gyro_e.rs b/crates/driver/src/domain/liquidity/balancer/v2/gyro_e.rs index b9a1584e24..06f27910d6 100644 --- a/crates/driver/src/domain/liquidity/balancer/v2/gyro_e.rs +++ b/crates/driver/src/domain/liquidity/balancer/v2/gyro_e.rs @@ -113,6 +113,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Signed fixed point number used for Gyroscope E-CLP parameters. diff --git a/crates/driver/src/domain/liquidity/balancer/v2/stable.rs b/crates/driver/src/domain/liquidity/balancer/v2/stable.rs index fbcdbbdb6d..e065fdbee7 100644 --- a/crates/driver/src/domain/liquidity/balancer/v2/stable.rs +++ b/crates/driver/src/domain/liquidity/balancer/v2/stable.rs @@ -95,6 +95,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Balancer V2 stable pool amplification parameter. diff --git a/crates/driver/src/domain/liquidity/balancer/v2/weighted.rs b/crates/driver/src/domain/liquidity/balancer/v2/weighted.rs index 53da2a6ac8..96c06d4e1c 100644 --- a/crates/driver/src/domain/liquidity/balancer/v2/weighted.rs +++ b/crates/driver/src/domain/liquidity/balancer/v2/weighted.rs @@ -102,6 +102,7 @@ pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, pub weight: Weight, + pub rate: eth::U256, } /// A Balancer token weight. diff --git a/crates/driver/src/domain/liquidity/balancer/v3/gyro_2clp.rs b/crates/driver/src/domain/liquidity/balancer/v3/gyro_2clp.rs index 0a8873c98c..4abfc5ea33 100644 --- a/crates/driver/src/domain/liquidity/balancer/v3/gyro_2clp.rs +++ b/crates/driver/src/domain/liquidity/balancer/v3/gyro_2clp.rs @@ -118,6 +118,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Signed fixed point number used for Gyroscope 2-CLP parameters. diff --git a/crates/driver/src/domain/liquidity/balancer/v3/gyro_e.rs b/crates/driver/src/domain/liquidity/balancer/v3/gyro_e.rs index 02da3a4469..aeca35af43 100644 --- a/crates/driver/src/domain/liquidity/balancer/v3/gyro_e.rs +++ b/crates/driver/src/domain/liquidity/balancer/v3/gyro_e.rs @@ -113,6 +113,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Signed fixed point number used for Gyroscope E-CLP parameters. diff --git a/crates/driver/src/domain/liquidity/balancer/v3/quantamm.rs b/crates/driver/src/domain/liquidity/balancer/v3/quantamm.rs index d654f12cb6..dbf6975a09 100644 --- a/crates/driver/src/domain/liquidity/balancer/v3/quantamm.rs +++ b/crates/driver/src/domain/liquidity/balancer/v3/quantamm.rs @@ -95,6 +95,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } #[derive(Clone, Copy, Debug)] diff --git a/crates/driver/src/domain/liquidity/balancer/v3/reclamm.rs b/crates/driver/src/domain/liquidity/balancer/v3/reclamm.rs index 6e5c56d421..1ab2be1051 100644 --- a/crates/driver/src/domain/liquidity/balancer/v3/reclamm.rs +++ b/crates/driver/src/domain/liquidity/balancer/v3/reclamm.rs @@ -89,6 +89,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } #[derive(Clone, Copy, Debug)] diff --git a/crates/driver/src/domain/liquidity/balancer/v3/stable.rs b/crates/driver/src/domain/liquidity/balancer/v3/stable.rs index 1b49805419..e39fec18d2 100644 --- a/crates/driver/src/domain/liquidity/balancer/v3/stable.rs +++ b/crates/driver/src/domain/liquidity/balancer/v3/stable.rs @@ -99,6 +99,7 @@ pub enum InvalidReserves { pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Balancer V3 stable pool amplification parameter. diff --git a/crates/driver/src/domain/liquidity/balancer/v3/weighted.rs b/crates/driver/src/domain/liquidity/balancer/v3/weighted.rs index fde83e2b71..769b093fda 100644 --- a/crates/driver/src/domain/liquidity/balancer/v3/weighted.rs +++ b/crates/driver/src/domain/liquidity/balancer/v3/weighted.rs @@ -97,6 +97,7 @@ pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, pub weight: Weight, + pub rate: eth::U256, } /// A Balancer V3 token weight. diff --git a/crates/driver/src/infra/solver/dto/auction.rs b/crates/driver/src/infra/solver/dto/auction.rs index 276b83af70..1d2fe5a295 100644 --- a/crates/driver/src/infra/solver/dto/auction.rs +++ b/crates/driver/src/infra/solver/dto/auction.rs @@ -230,7 +230,7 @@ pub fn new( solvers_dto::auction::Liquidity::Stable(solvers_dto::auction::StablePool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -241,6 +241,7 @@ pub fn new( solvers_dto::auction::StableReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -258,10 +259,7 @@ pub fn new( solvers_dto::auction::Liquidity::Stable(solvers_dto::auction::StablePool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -272,6 +270,7 @@ pub fn new( solvers_dto::auction::StableReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal_v3(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -290,7 +289,7 @@ pub fn new( solvers_dto::auction::WeightedProductPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -302,6 +301,7 @@ pub fn new( balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), weight: weight_to_decimal(r.weight), + rate: rate_to_decimal(r.rate), }, ) }) @@ -323,10 +323,7 @@ pub fn new( solvers_dto::auction::WeightedProductPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -340,6 +337,7 @@ pub fn new( r.scale, ), weight: weight_to_decimal_v3(r.weight), + rate: rate_to_decimal(r.rate), }, ) }) @@ -362,7 +360,7 @@ pub fn new( solvers_dto::auction::GyroEPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -373,6 +371,7 @@ pub fn new( solvers_dto::auction::GyroEReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -406,7 +405,7 @@ pub fn new( solvers_dto::auction::Gyro2CLPPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -417,6 +416,7 @@ pub fn new( solvers_dto::auction::Gyro2CLPReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -440,7 +440,7 @@ pub fn new( solvers_dto::auction::Gyro3CLPPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -451,6 +451,7 @@ pub fn new( solvers_dto::auction::Gyro3CLPReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -471,10 +472,7 @@ pub fn new( solvers_dto::auction::GyroEPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -487,6 +485,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -520,10 +519,7 @@ pub fn new( solvers_dto::auction::Gyro2CLPPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -536,6 +532,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -573,6 +570,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -606,10 +604,7 @@ pub fn new( solvers_dto::auction::QuantAmmPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -622,6 +617,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -713,10 +709,7 @@ pub fn new( solvers_dto::auction::StableSurgePool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -729,6 +722,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -855,13 +849,21 @@ fn weight_to_decimal_v3( fn scaling_factor_to_decimal( scale: liquidity::balancer::v2::ScalingFactor, ) -> bigdecimal::BigDecimal { - bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 18) + // Scaling factors are raw values (e.g., 10^12 for 6-decimal tokens), + // NOT in 18-decimal format like rates. Use exponent 0. + bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 0) } fn scaling_factor_to_decimal_v3( scale: liquidity::balancer::v3::ScalingFactor, ) -> bigdecimal::BigDecimal { - bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 18) + // Scaling factors are raw values (e.g., 10^12 for 6-decimal tokens), + // NOT in 18-decimal format like rates. Use exponent 0. + bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 0) +} + +fn rate_to_decimal(rate: eth::U256) -> bigdecimal::BigDecimal { + bigdecimal::BigDecimal::new(rate.to_big_int(), 18) } fn signed_fixed_point_to_decimal( diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_2clp.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_2clp.rs index 09220f0a14..b99a91a39c 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_2clp.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_2clp.rs @@ -34,6 +34,7 @@ pub fn to_domain(id: liquidity::Id, pool: Gyro2CLPPoolOrder) -> Result>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_3clp.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_3clp.rs index 4f269a967c..141b40bae2 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_3clp.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_3clp.rs @@ -33,6 +33,7 @@ pub fn to_domain(id: liquidity::Id, pool: Gyro3CLPPoolOrder) -> Result>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_e.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_e.rs index 4b1b68ad71..f35c52cade 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_e.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/gyro_e.rs @@ -34,6 +34,7 @@ pub fn to_domain(id: liquidity::Id, pool: GyroEPoolOrder) -> Result>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/stable.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/stable.rs index 1ce175a390..6290245f39 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/stable.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/stable.rs @@ -33,6 +33,7 @@ pub fn to_domain(id: liquidity::Id, pool: StablePoolOrder) -> Result>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/weighted.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/weighted.rs index 5d86fe77db..729ab4e69e 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/weighted.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v2/weighted.rs @@ -37,6 +37,7 @@ pub fn to_domain(id: liquidity::Id, pool: WeightedProductOrder) -> Result>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/gyro_2clp.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/gyro_2clp.rs index 4dbc165ba4..b7028f9456 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/gyro_2clp.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/gyro_2clp.rs @@ -32,6 +32,7 @@ pub fn to_domain(id: liquidity::Id, pool: BalancerV3Gyro2CLPOrder) -> Result>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/gyro_e.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/gyro_e.rs index 1df51e170b..423c5a9839 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/gyro_e.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/gyro_e.rs @@ -33,6 +33,7 @@ pub fn to_domain(id: liquidity::Id, pool: BalancerV3GyroEOrder) -> Result>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/quantamm.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/quantamm.rs index bc09c9e9e8..9ce4b735be 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/quantamm.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/quantamm.rs @@ -33,6 +33,7 @@ pub fn to_domain(id: liquidity::Id, pool: BalancerV3QuantAmmOrder) -> Result>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/reclamm.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/reclamm.rs index 98877833eb..343be5d182 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/reclamm.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/reclamm.rs @@ -32,6 +32,7 @@ pub fn to_domain(id: liquidity::Id, pool: BalancerV3ReClammOrder) -> Result>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/stable.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/stable.rs index d99c2a092d..3ce15cb2af 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/stable.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/stable.rs @@ -38,6 +38,7 @@ pub fn to_domain( scale: balancer::v3::ScalingFactor::from_raw( reserve.scaling_factor.as_uint256(), )?, + rate: reserve.rate.into(), }) }) .collect::>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/stable_surge.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/stable_surge.rs index ce4cdcf120..79697e054e 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/stable_surge.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/stable_surge.rs @@ -40,6 +40,7 @@ pub fn to_domain( scale: balancer::v3::ScalingFactor::from_raw( reserve.scaling_factor.as_uint256(), )?, + rate: reserve.rate.into(), }) }) .collect::>()?, diff --git a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/weighted.rs b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/weighted.rs index 64574f0aad..9365689a8d 100644 --- a/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/weighted.rs +++ b/crates/liquidity-driver/src/boundary/liquidity/balancer/v3/weighted.rs @@ -39,6 +39,7 @@ pub fn to_domain( scale: balancer::v3::ScalingFactor::from_raw( reserve.common.scaling_factor.as_uint256(), )?, + rate: reserve.common.rate.into(), }) }) .collect::>()?, diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_2clp.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_2clp.rs index 2766cbb157..5cd80ad197 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_2clp.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_2clp.rs @@ -103,6 +103,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Signed fixed point number used for Gyroscope 2-CLP parameters. diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_3clp.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_3clp.rs index 5f076922f1..6eab560102 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_3clp.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_3clp.rs @@ -108,6 +108,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Fixed point number used for Gyroscope 3-CLP parameters. diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_e.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_e.rs index b9a1584e24..06f27910d6 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_e.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/gyro_e.rs @@ -113,6 +113,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Signed fixed point number used for Gyroscope E-CLP parameters. diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/stable.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/stable.rs index fbcdbbdb6d..e065fdbee7 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/stable.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/stable.rs @@ -95,6 +95,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Balancer V2 stable pool amplification parameter. diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/weighted.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/weighted.rs index 53da2a6ac8..96c06d4e1c 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v2/weighted.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v2/weighted.rs @@ -102,6 +102,7 @@ pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, pub weight: Weight, + pub rate: eth::U256, } /// A Balancer token weight. diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/gyro_2clp.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/gyro_2clp.rs index 0a8873c98c..4abfc5ea33 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/gyro_2clp.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/gyro_2clp.rs @@ -118,6 +118,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Signed fixed point number used for Gyroscope 2-CLP parameters. diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/gyro_e.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/gyro_e.rs index 02da3a4469..aeca35af43 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/gyro_e.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/gyro_e.rs @@ -113,6 +113,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Signed fixed point number used for Gyroscope E-CLP parameters. diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/quantamm.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/quantamm.rs index d654f12cb6..dbf6975a09 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/quantamm.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/quantamm.rs @@ -95,6 +95,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } #[derive(Clone, Copy, Debug)] diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/reclamm.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/reclamm.rs index 6e5c56d421..1ab2be1051 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/reclamm.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/reclamm.rs @@ -89,6 +89,7 @@ pub struct InvalidReserves; pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } #[derive(Clone, Copy, Debug)] diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/stable.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/stable.rs index 1b49805419..e39fec18d2 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/stable.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/stable.rs @@ -99,6 +99,7 @@ pub enum InvalidReserves { pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, + pub rate: eth::U256, } /// Balancer V3 stable pool amplification parameter. diff --git a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/weighted.rs b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/weighted.rs index fde83e2b71..769b093fda 100644 --- a/crates/liquidity-driver/src/domain/liquidity/balancer/v3/weighted.rs +++ b/crates/liquidity-driver/src/domain/liquidity/balancer/v3/weighted.rs @@ -97,6 +97,7 @@ pub struct Reserve { pub asset: eth::Asset, pub scale: ScalingFactor, pub weight: Weight, + pub rate: eth::U256, } /// A Balancer V3 token weight. diff --git a/crates/liquidity-driver/src/infra/api/routes/liquidity/mod.rs b/crates/liquidity-driver/src/infra/api/routes/liquidity/mod.rs index 197c9375ec..c3d2a07108 100644 --- a/crates/liquidity-driver/src/infra/api/routes/liquidity/mod.rs +++ b/crates/liquidity-driver/src/infra/api/routes/liquidity/mod.rs @@ -132,7 +132,7 @@ fn convert_domain_to_dto( solvers_dto::auction::WeightedProductPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -144,6 +144,7 @@ fn convert_domain_to_dto( balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), weight: weight_to_decimal(r.weight), + rate: rate_to_decimal(r.rate), }, ) }) @@ -166,10 +167,7 @@ fn convert_domain_to_dto( solvers_dto::auction::WeightedProductPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -181,6 +179,7 @@ fn convert_domain_to_dto( balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal_v3(r.scale), weight: weight_to_decimal_v3(r.weight), + rate: rate_to_decimal(r.rate), }, ) }) @@ -200,7 +199,7 @@ fn convert_domain_to_dto( solvers_dto::auction::StablePool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -211,6 +210,7 @@ fn convert_domain_to_dto( solvers_dto::auction::StableReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -227,10 +227,7 @@ fn convert_domain_to_dto( solvers_dto::auction::StablePool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -241,6 +238,7 @@ fn convert_domain_to_dto( solvers_dto::auction::StableReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal_v3(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -257,10 +255,7 @@ fn convert_domain_to_dto( solvers_dto::auction::Liquidity::StableSurge(solvers_dto::auction::StableSurgePool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -271,6 +266,7 @@ fn convert_domain_to_dto( solvers_dto::auction::StableReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal_v3(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -293,7 +289,7 @@ fn convert_domain_to_dto( Box::new(solvers_dto::auction::GyroEPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -304,6 +300,7 @@ fn convert_domain_to_dto( solvers_dto::auction::GyroEReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -335,7 +332,7 @@ fn convert_domain_to_dto( solvers_dto::auction::Gyro2CLPPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -346,6 +343,7 @@ fn convert_domain_to_dto( solvers_dto::auction::Gyro2CLPReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -365,7 +363,7 @@ fn convert_domain_to_dto( solvers_dto::auction::Gyro3CLPPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -376,6 +374,7 @@ fn convert_domain_to_dto( solvers_dto::auction::Gyro3CLPReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -394,10 +393,7 @@ fn convert_domain_to_dto( Box::new(solvers_dto::auction::GyroEPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -408,6 +404,7 @@ fn convert_domain_to_dto( solvers_dto::auction::GyroEReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal_v3(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -439,10 +436,7 @@ fn convert_domain_to_dto( solvers_dto::auction::Gyro2CLPPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -453,6 +447,7 @@ fn convert_domain_to_dto( solvers_dto::auction::Gyro2CLPReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal_v3(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -482,6 +477,7 @@ fn convert_domain_to_dto( solvers_dto::auction::ReClammReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal_v3(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -510,10 +506,7 @@ fn convert_domain_to_dto( solvers_dto::auction::QuantAmmPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.0.into(), tokens: pool .reserves @@ -524,6 +517,7 @@ fn convert_domain_to_dto( solvers_dto::auction::QuantAmmReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal_v3(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -637,13 +631,21 @@ fn weight_to_decimal_v3( fn scaling_factor_to_decimal( scale: liquidity::balancer::v2::ScalingFactor, ) -> bigdecimal::BigDecimal { - bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 18) + // Scaling factors are raw values (e.g., 10^12 for 6-decimal tokens), + // NOT in 18-decimal format like rates. Use exponent 0. + bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 0) } fn scaling_factor_to_decimal_v3( scale: liquidity::balancer::v3::ScalingFactor, ) -> bigdecimal::BigDecimal { - bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 18) + // Scaling factors are raw values (e.g., 10^12 for 6-decimal tokens), + // NOT in 18-decimal format like rates. Use exponent 0. + bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 0) +} + +fn rate_to_decimal(rate: eth::U256) -> bigdecimal::BigDecimal { + bigdecimal::BigDecimal::new(rate.to_big_int(), 18) } fn surge_threshold_to_decimal_v3( diff --git a/crates/liquidity-driver/src/infra/solver/dto/auction.rs b/crates/liquidity-driver/src/infra/solver/dto/auction.rs index 276b83af70..1d2fe5a295 100644 --- a/crates/liquidity-driver/src/infra/solver/dto/auction.rs +++ b/crates/liquidity-driver/src/infra/solver/dto/auction.rs @@ -230,7 +230,7 @@ pub fn new( solvers_dto::auction::Liquidity::Stable(solvers_dto::auction::StablePool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -241,6 +241,7 @@ pub fn new( solvers_dto::auction::StableReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -258,10 +259,7 @@ pub fn new( solvers_dto::auction::Liquidity::Stable(solvers_dto::auction::StablePool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -272,6 +270,7 @@ pub fn new( solvers_dto::auction::StableReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal_v3(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -290,7 +289,7 @@ pub fn new( solvers_dto::auction::WeightedProductPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -302,6 +301,7 @@ pub fn new( balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), weight: weight_to_decimal(r.weight), + rate: rate_to_decimal(r.rate), }, ) }) @@ -323,10 +323,7 @@ pub fn new( solvers_dto::auction::WeightedProductPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -340,6 +337,7 @@ pub fn new( r.scale, ), weight: weight_to_decimal_v3(r.weight), + rate: rate_to_decimal(r.rate), }, ) }) @@ -362,7 +360,7 @@ pub fn new( solvers_dto::auction::GyroEPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -373,6 +371,7 @@ pub fn new( solvers_dto::auction::GyroEReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -406,7 +405,7 @@ pub fn new( solvers_dto::auction::Gyro2CLPPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -417,6 +416,7 @@ pub fn new( solvers_dto::auction::Gyro2CLPReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -440,7 +440,7 @@ pub fn new( solvers_dto::auction::Gyro3CLPPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: pool.id.into(), + balancer_pool_id: Some(pool.id.into()), gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -451,6 +451,7 @@ pub fn new( solvers_dto::auction::Gyro3CLPReserve { balance: r.asset.amount.into(), scaling_factor: scaling_factor_to_decimal(r.scale), + rate: rate_to_decimal(r.rate), }, ) }) @@ -471,10 +472,7 @@ pub fn new( solvers_dto::auction::GyroEPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -487,6 +485,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -520,10 +519,7 @@ pub fn new( solvers_dto::auction::Gyro2CLPPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -536,6 +532,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -573,6 +570,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -606,10 +604,7 @@ pub fn new( solvers_dto::auction::QuantAmmPool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -622,6 +617,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -713,10 +709,7 @@ pub fn new( solvers_dto::auction::StableSurgePool { id: liquidity.id.0.to_string(), address: pool.id.address().into(), - balancer_pool_id: { - let pool_id_h160: eth::H160 = pool.id.into(); - pool_id_h160.into() - }, + balancer_pool_id: None, // V3 pools don't have V2-style pool IDs gas_estimate: liquidity.gas.into(), tokens: pool .reserves @@ -729,6 +722,7 @@ pub fn new( scaling_factor: scaling_factor_to_decimal_v3( r.scale, ), + rate: rate_to_decimal(r.rate), }, ) }) @@ -855,13 +849,21 @@ fn weight_to_decimal_v3( fn scaling_factor_to_decimal( scale: liquidity::balancer::v2::ScalingFactor, ) -> bigdecimal::BigDecimal { - bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 18) + // Scaling factors are raw values (e.g., 10^12 for 6-decimal tokens), + // NOT in 18-decimal format like rates. Use exponent 0. + bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 0) } fn scaling_factor_to_decimal_v3( scale: liquidity::balancer::v3::ScalingFactor, ) -> bigdecimal::BigDecimal { - bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 18) + // Scaling factors are raw values (e.g., 10^12 for 6-decimal tokens), + // NOT in 18-decimal format like rates. Use exponent 0. + bigdecimal::BigDecimal::new(scale.as_raw().to_big_int(), 0) +} + +fn rate_to_decimal(rate: eth::U256) -> bigdecimal::BigDecimal { + bigdecimal::BigDecimal::new(rate.to_big_int(), 18) } fn signed_fixed_point_to_decimal( diff --git a/crates/shared/src/sources/balancer_v2/pools/composable_stable.rs b/crates/shared/src/sources/balancer_v2/pools/composable_stable.rs index b8fc359018..4d1a1790ec 100644 --- a/crates/shared/src/sources/balancer_v2/pools/composable_stable.rs +++ b/crates/shared/src/sources/balancer_v2/pools/composable_stable.rs @@ -95,8 +95,12 @@ impl FactoryIndexing for BalancerV2ComposableStablePoolFactory::Instance { ( address, common::TokenState { + balance: token.balance, scaling_factor: Bfp::from_wei(scaling_factor.into_legacy()), - ..token + // For composable stable pools, getScalingFactors() returns + // scaling factors that already include the rate provider rate. + // We set rate to 1.0 to avoid double-applying the rate. + rate: ethcontract::U256::exp10(18), }, ) }) diff --git a/crates/solvers-dto/src/auction.rs b/crates/solvers-dto/src/auction.rs index a6fd8b2a6b..054c702173 100644 --- a/crates/solvers-dto/src/auction.rs +++ b/crates/solvers-dto/src/auction.rs @@ -203,7 +203,8 @@ pub struct ConstantProductReserve { pub struct WeightedProductPool { pub id: String, pub address: H160, - pub balancer_pool_id: H256, + #[serde(skip_serializing_if = "Option::is_none")] + pub balancer_pool_id: Option, #[serde_as(as = "HexOrDecimalU256")] pub gas_estimate: U256, pub tokens: HashMap, @@ -219,6 +220,7 @@ pub struct WeightedProductReserve { pub balance: U256, pub scaling_factor: BigDecimal, pub weight: BigDecimal, + pub rate: BigDecimal, } #[derive(Debug, Serialize, Deserialize)] @@ -234,7 +236,8 @@ pub enum WeightedProductVersion { pub struct StablePool { pub id: String, pub address: H160, - pub balancer_pool_id: H256, + #[serde(skip_serializing_if = "Option::is_none")] + pub balancer_pool_id: Option, #[serde_as(as = "HexOrDecimalU256")] pub gas_estimate: U256, pub tokens: HashMap, @@ -249,6 +252,7 @@ pub struct StableReserve { #[serde_as(as = "HexOrDecimalU256")] pub balance: U256, pub scaling_factor: BigDecimal, + pub rate: BigDecimal, } #[serde_as] @@ -257,7 +261,8 @@ pub struct StableReserve { pub struct StableSurgePool { pub id: String, pub address: H160, - pub balancer_pool_id: H256, + #[serde(skip_serializing_if = "Option::is_none")] + pub balancer_pool_id: Option, #[serde_as(as = "HexOrDecimalU256")] pub gas_estimate: U256, pub tokens: HashMap, @@ -326,7 +331,8 @@ pub struct Erc4626Edge { pub struct GyroEPool { pub id: String, pub address: H160, - pub balancer_pool_id: H256, + #[serde(skip_serializing_if = "Option::is_none")] + pub balancer_pool_id: Option, #[serde_as(as = "HexOrDecimalU256")] pub gas_estimate: U256, pub tokens: HashMap, @@ -356,6 +362,7 @@ pub struct GyroEReserve { #[serde_as(as = "HexOrDecimalU256")] pub balance: U256, pub scaling_factor: BigDecimal, + pub rate: BigDecimal, } #[derive(Debug, Serialize, Deserialize)] @@ -370,7 +377,8 @@ pub enum GyroEVersion { pub struct Gyro2CLPPool { pub id: String, pub address: H160, - pub balancer_pool_id: H256, + #[serde(skip_serializing_if = "Option::is_none")] + pub balancer_pool_id: Option, #[serde_as(as = "HexOrDecimalU256")] pub gas_estimate: U256, pub tokens: HashMap, @@ -387,7 +395,8 @@ pub struct Gyro2CLPPool { pub struct Gyro3CLPPool { pub id: String, pub address: H160, - pub balancer_pool_id: H256, + #[serde(skip_serializing_if = "Option::is_none")] + pub balancer_pool_id: Option, #[serde_as(as = "HexOrDecimalU256")] pub gas_estimate: U256, pub tokens: HashMap, @@ -404,6 +413,7 @@ pub struct Gyro2CLPReserve { #[serde_as(as = "HexOrDecimalU256")] pub balance: U256, pub scaling_factor: BigDecimal, + pub rate: BigDecimal, } #[derive(Debug, Serialize, Deserialize)] @@ -419,6 +429,7 @@ pub struct Gyro3CLPReserve { #[serde_as(as = "HexOrDecimalU256")] pub balance: U256, pub scaling_factor: BigDecimal, + pub rate: BigDecimal, } #[derive(Debug, Serialize, Deserialize)] @@ -455,6 +466,7 @@ pub struct ReClammReserve { #[serde_as(as = "HexOrDecimalU256")] pub balance: U256, pub scaling_factor: BigDecimal, + pub rate: BigDecimal, } #[serde_as] @@ -463,7 +475,8 @@ pub struct ReClammReserve { pub struct QuantAmmPool { pub id: String, pub address: H160, - pub balancer_pool_id: H256, + #[serde(skip_serializing_if = "Option::is_none")] + pub balancer_pool_id: Option, #[serde_as(as = "HexOrDecimalU256")] pub gas_estimate: U256, pub tokens: HashMap, @@ -485,6 +498,7 @@ pub struct QuantAmmReserve { #[serde_as(as = "HexOrDecimalU256")] pub balance: U256, pub scaling_factor: BigDecimal, + pub rate: BigDecimal, } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/solvers/openapi.yml b/crates/solvers/openapi.yml index 24025a7123..045e3226e8 100644 --- a/crates/solvers/openapi.yml +++ b/crates/solvers/openapi.yml @@ -518,7 +518,6 @@ components: - kind - tokens - fee - - balancer_pool_id properties: kind: type: string @@ -557,7 +556,6 @@ components: - tokens - amplificationParameter - fee - - balancer_pool_id properties: kind: type: string diff --git a/crates/solvers/src/api/routes/solve/dto/auction.rs b/crates/solvers/src/api/routes/solve/dto/auction.rs index ceab817135..00320505d8 100644 --- a/crates/solvers/src/api/routes/solve/dto/auction.rs +++ b/crates/solvers/src/api/routes/solve/dto/auction.rs @@ -163,6 +163,7 @@ mod weighted_product_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -202,6 +203,7 @@ mod stable_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -301,6 +303,7 @@ mod gyro_e_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -364,6 +367,7 @@ mod gyro_2clp_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -409,6 +413,7 @@ mod gyro_3clp_pool { .ok_or("invalid scaling factor")?, ) .ok_or("invalid scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -447,6 +452,7 @@ mod reclamm_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -506,6 +512,7 @@ mod stable_surge_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; @@ -547,6 +554,7 @@ mod quant_amm_pool { scale: conv::decimal_to_rational(&token.scaling_factor) .and_then(liquidity::ScalingFactor::new) .ok_or("invalid token scaling factor")?, + rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?, }) }) .collect::, Error>>()?; diff --git a/crates/solvers/src/boundary/liquidity/gyro_2clp.rs b/crates/solvers/src/boundary/liquidity/gyro_2clp.rs index 81c9c99f6a..b3c455b5c4 100644 --- a/crates/solvers/src/boundary/liquidity/gyro_2clp.rs +++ b/crates/solvers/src/boundary/liquidity/gyro_2clp.rs @@ -26,7 +26,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::gyro_2clp::Pool) -> Opt TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -66,3 +66,10 @@ fn to_signed_fixed_point(ratio: ð::SignedRational) -> Option { let scaled = ratio.numer().checked_mul(base)? / *ratio.denom(); Some(SBfp::from_wei(scaled)) } + +/// Converts a rational to a U256. +/// Note: Rate is already in wei (18 decimals), so we just convert the rational +/// directly. +fn to_u256(ratio: ð::Rational) -> Option { + ratio.numer().checked_div(*ratio.denom()) +} diff --git a/crates/solvers/src/boundary/liquidity/gyro_3clp.rs b/crates/solvers/src/boundary/liquidity/gyro_3clp.rs index 8a306c6c68..7864e2ad7a 100644 --- a/crates/solvers/src/boundary/liquidity/gyro_3clp.rs +++ b/crates/solvers/src/boundary/liquidity/gyro_3clp.rs @@ -31,7 +31,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::gyro_3clp::Pool) -> Opt TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -62,3 +62,10 @@ fn to_fixed_point(ratio: &crate::domain::eth::Rational) -> Option { let wei = ratio.numer().checked_mul(base)? / ratio.denom(); Some(Bfp::from_wei(wei)) } + +/// Converts a rational to a U256. +/// Note: Rate is already in wei (18 decimals), so we just convert the rational +/// directly. +fn to_u256(ratio: &crate::domain::eth::Rational) -> Option { + ratio.numer().checked_div(*ratio.denom()) +} diff --git a/crates/solvers/src/boundary/liquidity/gyro_e.rs b/crates/solvers/src/boundary/liquidity/gyro_e.rs index dd6537a2ca..bbe457678e 100644 --- a/crates/solvers/src/boundary/liquidity/gyro_e.rs +++ b/crates/solvers/src/boundary/liquidity/gyro_e.rs @@ -31,7 +31,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::gyro_e::Pool) -> Option TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -93,3 +93,10 @@ fn to_signed_fixed_point(ratio: ð::SignedRational) -> Option { Some(SBfp::from_wei(wei_i256)) } + +/// Converts a rational to a U256. +/// Note: Rate is already in wei (18 decimals), so we just convert the rational +/// directly. +fn to_u256(ratio: ð::Rational) -> Option { + ratio.numer().checked_div(*ratio.denom()) +} diff --git a/crates/solvers/src/boundary/liquidity/quantamm.rs b/crates/solvers/src/boundary/liquidity/quantamm.rs index b2d4ccabe4..f3655b3c2b 100644 --- a/crates/solvers/src/boundary/liquidity/quantamm.rs +++ b/crates/solvers/src/boundary/liquidity/quantamm.rs @@ -28,7 +28,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::quantamm::Pool) -> Opti TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -61,6 +61,12 @@ fn to_fixed_point(ratio: ð::Rational) -> Option { Some(Bfp::from_wei(wei)) } +fn to_u256(ratio: ð::Rational) -> Option { + // Rate is already in wei (18 decimals), so we just convert the rational + // directly. + ratio.numer().checked_div(*ratio.denom()) +} + fn to_signed_i256(ratio: ð::SignedRational) -> Option { // Follow the same pattern as GyroE's to_signed_fixed_point function let base = I256::from(10u64.pow(18)); diff --git a/crates/solvers/src/boundary/liquidity/reclamm.rs b/crates/solvers/src/boundary/liquidity/reclamm.rs index 245682181b..38afce8b75 100644 --- a/crates/solvers/src/boundary/liquidity/reclamm.rs +++ b/crates/solvers/src/boundary/liquidity/reclamm.rs @@ -27,7 +27,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::reclamm::Pool) -> Optio TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -57,3 +57,9 @@ fn to_fixed_point(ratio: ð::Rational) -> Option { let wei = ratio.numer().checked_mul(base)? / ratio.denom(); Some(Bfp::from_wei(wei)) } + +fn to_u256(ratio: ð::Rational) -> Option { + // Rate is already in wei (18 decimals), so we just convert the rational + // directly. + ratio.numer().checked_div(*ratio.denom()) +} diff --git a/crates/solvers/src/boundary/liquidity/stable.rs b/crates/solvers/src/boundary/liquidity/stable.rs index f7f85954ac..eeff6e776e 100644 --- a/crates/solvers/src/boundary/liquidity/stable.rs +++ b/crates/solvers/src/boundary/liquidity/stable.rs @@ -31,7 +31,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::stable::Pool) -> Option TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, )) }) @@ -63,3 +63,10 @@ fn to_fixed_point(ratio: ð::Rational) -> Option { let wei = ratio.numer().checked_mul(base)? / ratio.denom(); Some(Bfp::from_wei(wei)) } + +/// Converts a rational to a U256. +/// Note: Rate is already in wei (18 decimals), so we just convert the rational +/// directly. +fn to_u256(ratio: ð::Rational) -> Option { + ratio.numer().checked_div(*ratio.denom()) +} diff --git a/crates/solvers/src/boundary/liquidity/weighted_product.rs b/crates/solvers/src/boundary/liquidity/weighted_product.rs index a4552c9c53..0205db1874 100644 --- a/crates/solvers/src/boundary/liquidity/weighted_product.rs +++ b/crates/solvers/src/boundary/liquidity/weighted_product.rs @@ -32,7 +32,7 @@ pub fn to_boundary_pool(address: H160, pool: &liquidity::weighted_product::Pool) common: TokenState { balance: reserve.asset.amount, scaling_factor: to_fixed_point(&reserve.scale.get())?, - rate: U256::exp10(18), + rate: to_u256(&reserve.rate)?, }, weight: to_fixed_point(&reserve.weight)?, }, @@ -64,3 +64,10 @@ fn to_fixed_point(ratio: ð::Rational) -> Option { let wei = ratio.numer().checked_mul(base)? / ratio.denom(); Some(Bfp::from_wei(wei)) } + +/// Converts a rational to a U256. +/// Note: Rate is already in wei (18 decimals), so we just convert the rational +/// directly. +fn to_u256(ratio: ð::Rational) -> Option { + ratio.numer().checked_div(*ratio.denom()) +} diff --git a/crates/solvers/src/domain/liquidity/gyro_2clp.rs b/crates/solvers/src/domain/liquidity/gyro_2clp.rs index 9244afd9c7..ee5590bb80 100644 --- a/crates/solvers/src/domain/liquidity/gyro_2clp.rs +++ b/crates/solvers/src/domain/liquidity/gyro_2clp.rs @@ -60,6 +60,7 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The Gyroscope 2-CLP pool version. diff --git a/crates/solvers/src/domain/liquidity/gyro_3clp.rs b/crates/solvers/src/domain/liquidity/gyro_3clp.rs index e20f728433..de73083f17 100644 --- a/crates/solvers/src/domain/liquidity/gyro_3clp.rs +++ b/crates/solvers/src/domain/liquidity/gyro_3clp.rs @@ -66,6 +66,7 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The Gyroscope 3-CLP pool version. diff --git a/crates/solvers/src/domain/liquidity/gyro_e.rs b/crates/solvers/src/domain/liquidity/gyro_e.rs index 0cf93772b2..35ef58d935 100644 --- a/crates/solvers/src/domain/liquidity/gyro_e.rs +++ b/crates/solvers/src/domain/liquidity/gyro_e.rs @@ -72,6 +72,7 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The Gyroscope E-CLP pool version. diff --git a/crates/solvers/src/domain/liquidity/quantamm.rs b/crates/solvers/src/domain/liquidity/quantamm.rs index 1e7033edf1..e0526f53bd 100644 --- a/crates/solvers/src/domain/liquidity/quantamm.rs +++ b/crates/solvers/src/domain/liquidity/quantamm.rs @@ -59,6 +59,7 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The QuantAMM pool version. diff --git a/crates/solvers/src/domain/liquidity/reclamm.rs b/crates/solvers/src/domain/liquidity/reclamm.rs index 5b024846bc..660e62bbd5 100644 --- a/crates/solvers/src/domain/liquidity/reclamm.rs +++ b/crates/solvers/src/domain/liquidity/reclamm.rs @@ -48,4 +48,5 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: super::ScalingFactor, + pub rate: eth::Rational, } diff --git a/crates/solvers/src/domain/liquidity/stable.rs b/crates/solvers/src/domain/liquidity/stable.rs index 81eeb6bd3d..3848034f9d 100644 --- a/crates/solvers/src/domain/liquidity/stable.rs +++ b/crates/solvers/src/domain/liquidity/stable.rs @@ -56,4 +56,5 @@ impl Reserves { pub struct Reserve { pub asset: eth::Asset, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } diff --git a/crates/solvers/src/domain/liquidity/weighted_product.rs b/crates/solvers/src/domain/liquidity/weighted_product.rs index 730369b050..513869fcf1 100644 --- a/crates/solvers/src/domain/liquidity/weighted_product.rs +++ b/crates/solvers/src/domain/liquidity/weighted_product.rs @@ -57,6 +57,7 @@ pub struct Reserve { pub asset: eth::Asset, pub weight: eth::Rational, pub scale: liquidity::ScalingFactor, + pub rate: eth::Rational, } /// The version of the weighted product math to use. Different versions have diff --git a/crates/solvers/src/tests/cases/bal_liquidity.rs b/crates/solvers/src/tests/cases/bal_liquidity.rs index 6e74eb830c..499781da39 100644 --- a/crates/solvers/src/tests/cases/bal_liquidity.rs +++ b/crates/solvers/src/tests/cases/bal_liquidity.rs @@ -78,12 +78,16 @@ async fn weighted() { "0x6810e776880c02933d47db1b9fc05908e5386b96": { "balance": "11260752191375725565253", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab": { "balance": "18764168403990393422000071", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -206,12 +210,16 @@ async fn weighted_v3plus() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "5089632258314443812936111", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "3043530764763263654069", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -381,14 +389,17 @@ async fn stable() { "0x6b175474e89094c44da98b954eedeac495271d0f": { "balance": "505781036390938593206504", "scalingFactor": "1", + "rate": "1000000000000000000", }, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { "balance": "554894862074", "scalingFactor": "1000000000000", + "rate": "1000000000000000000", }, "0xdac17f958d2ee523a2206206994597c13d831ec7": { "balance": "1585576741011", "scalingFactor": "1000000000000", + "rate": "1000000000000000000", }, }, "fee": "0.0001", @@ -557,14 +568,17 @@ async fn composable_stable_v4() { "0x4b1e2c2762667331bc91648052f646d1b0d35984": { "balance": "126041615528606990697699", "scalingFactor": "1", + "rate": "1000000000000000000", }, "0x5c78d05b8ecf97507d1cf70646082c54faa4da95": { "balance": "2596148429267369423681023550322451", "scalingFactor": "1", + "rate": "1000000000000000000", }, "0xcb444e90d8198415266c6a2724b7900fb12fc56e": { "balance": "170162457652825667152980", "scalingFactor": "1", + "rate": "1000000000000000000", }, }, "fee": "0.0001", diff --git a/crates/solvers/src/tests/cases/buy_order_rounding.rs b/crates/solvers/src/tests/cases/buy_order_rounding.rs index b0d96c60a1..0c7332dbf5 100644 --- a/crates/solvers/src/tests/cases/buy_order_rounding.rs +++ b/crates/solvers/src/tests/cases/buy_order_rounding.rs @@ -214,12 +214,16 @@ async fn balancer_weighted() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "1963528800698237927834721", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "1152796145430714835825", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -370,12 +374,16 @@ async fn balancer_weighted_v3plus() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "18764168403990393422000071", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "11260752191375725565253", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -514,12 +522,16 @@ async fn distant_convergence() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "5089632258314443812936111", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "3043530764763263654069", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -658,12 +670,16 @@ async fn same_path() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "1963528800698237927834721", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "1152796145430714835825", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -852,14 +868,17 @@ async fn balancer_stable() { "0x4b1e2c2762667331bc91648052f646d1b0d35984": { "balance": "126041615528606990697699", "scalingFactor": "1", + "rate": "1000000000000000000", }, "0x5c78d05b8ecf97507d1cf70646082c54faa4da95": { "balance": "2596148429267369423681023550322451", "scalingFactor": "1", + "rate": "1000000000000000000", }, "0xcb444e90d8198415266c6a2724b7900fb12fc56e": { "balance": "170162457652825667152980", "scalingFactor": "1", + "rate": "1000000000000000000", }, }, "fee": "0.0001", diff --git a/crates/solvers/src/tests/cases/limit_order_quoting.rs b/crates/solvers/src/tests/cases/limit_order_quoting.rs index fec623e088..057bf36a98 100644 --- a/crates/solvers/src/tests/cases/limit_order_quoting.rs +++ b/crates/solvers/src/tests/cases/limit_order_quoting.rs @@ -71,12 +71,16 @@ async fn sell_order() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "5089632258314443812936111", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "3043530764763263654069", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005", @@ -216,12 +220,16 @@ async fn buy_order() { "0x177127622c4a00f3d409b75571e12cb3c8973d3c": { "balance": "5089632258314443812936111", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", }, "0x9c58bacc331c9aa871afd802db6379a98e80cedb": { "balance": "3043530764763263654069", "scalingFactor": "1", + "rate": "1000000000000000000", "weight": "0.5", + "rate": "1000000000000000000", } }, "fee": "0.005",