[feature] implement unified order simulation logic#4127
[feature] implement unified order simulation logic#4127theghostmac wants to merge 10 commits intocowprotocol:mainfrom
Conversation
- Implement settle() call simulation with state overrides - Support EIP-1271 signature validation - Track wrapper/flashloan/pre-post-hook execution - Add balance override support for buy token
There was a problem hiding this comment.
Code Review
This pull request introduces an order execution simulator. The review has identified several high-severity issues: an incorrect clearing price calculation for same-token trades in the simulation, an incorrect fee amount being used for the simulation order, and a potential panic due to an unwrap() on a Result in the factory.
crates/shared/src/order_quoting.rs
Outdated
| buy_amount: quoted_buy_amount, | ||
| valid_to: u32::MAX, // Simulation doesn't care about time | ||
| app_data: AppDataHash::default(), | ||
| fee_amount: U256::from(trade_estimate.gas), |
There was a problem hiding this comment.
The fee_amount for the simulation order is incorrectly set to U256::from(trade_estimate.gas). The fee amount should be in units of the sell token, not gas units. This will lead to incorrect simulation results. The correct fee amount should be calculated from the fee_parameters which are available in this scope.
| fee_amount: U256::from(trade_estimate.gas), | |
| fee_amount: fee_parameters.fee(), |
| .shared_args | ||
| .tenderly | ||
| .get_api_instance(&self.components.http_factory, "order_simulation".to_owned()) | ||
| .unwrap() |
There was a problem hiding this comment.
| let tokens = if order.data.sell_token < order.data.buy_token { | ||
| vec![order.data.sell_token, order.data.buy_token] | ||
| } else { | ||
| vec![order.data.buy_token, order.data.sell_token] | ||
| }; | ||
|
|
||
| let sell_token_index = tokens | ||
| .iter() | ||
| .position(|&t| t == order.data.sell_token) | ||
| .unwrap(); | ||
|
|
||
| let buy_token_index = tokens | ||
| .iter() | ||
| .position(|&t| t == order.data.buy_token) | ||
| .unwrap(); | ||
|
|
||
| // Clearing prices are set such that the order is settled exactly at its limit price. | ||
| let mut clearing_prices = vec![U256::ZERO; 2]; | ||
| clearing_prices[sell_token_index] = order.data.buy_amount; | ||
| clearing_prices[buy_token_index] = order.data.sell_amount; |
There was a problem hiding this comment.
The current logic for encoding a settlement does not correctly handle same-token trades (where sell_token == buy_token). When tokens are the same, the tokens vector contains duplicates, and the clearing price calculation overwrites itself, leading to an incorrect price. This will cause simulation failures for same-token trades. The logic should be updated to handle this case by creating a unique list of tokens and setting a 1:1 clearing price for same-token trades.
let tokens = {
let mut tokens = vec![order.data.sell_token, order.data.buy_token];
tokens.sort();
tokens.dedup();
tokens
};
let sell_token_index = tokens
.iter()
.position(|&t| t == order.data.sell_token)
.unwrap();
let buy_token_index = tokens
.iter()
.position(|&t| t == order.data.buy_token)
.unwrap();
// Clearing prices are set such that the order is settled exactly at its limit price.
// For same-token trades, the price is 1:1.
let clearing_prices = if tokens.len() == 1 {
vec![U256::from(1)]
} else {
let mut prices = vec![U256::ZERO; tokens.len()];
prices[sell_token_index] = order.data.buy_amount;
prices[buy_token_index] = order.data.sell_amount;
prices
};|
i was blocked on running integration tests for this, so i spent time reading through e2e/tests and discovered i realized i can just run doing so, i ran into signature validation issues - the settlement contract validates signatures, but simulation happens before the order is actually signed. i removed the premature integration into the quote flow and kept the simulator as a standalone component ready for order validation (not quote validation, which the local e2e test passes. the forked test requires |
There was a problem hiding this comment.
Code Review
This pull request introduces an OrderExecutionSimulator for end-to-end order validation. My review focuses on improving the robustness of the service initialization. Specifically, I've identified that a misconfiguration of the optional Tenderly integration could cause the service to panic on startup. I've suggested changes to handle this error gracefully to prevent the service from crashing.
| let tenderly = args | ||
| .shared | ||
| .tenderly | ||
| .get_api_instance(&http_factory, "order_simulation".to_owned()) | ||
| .unwrap() | ||
| .map(|t| Arc::new(shared::tenderly_api::TenderlyCodeSimulator::new(t, chain.id()))); |
There was a problem hiding this comment.
Using .unwrap() here will cause the service to panic on startup if the Tenderly API is misconfigured. Since Tenderly is used for logging and is not a critical component, it would be more robust to handle the error gracefully, log a warning, and continue without Tenderly simulation. This would prevent a misconfiguration in an optional component from taking down the entire service.
| let tenderly = args | |
| .shared | |
| .tenderly | |
| .get_api_instance(&http_factory, "order_simulation".to_owned()) | |
| .unwrap() | |
| .map(|t| Arc::new(shared::tenderly_api::TenderlyCodeSimulator::new(t, chain.id()))); | |
| let tenderly = args.shared.tenderly.get_api_instance(&http_factory, "order_simulation".to_owned()) | |
| .unwrap_or_else(|err| { | |
| tracing::warn!(?err, "failed to initialize tenderly api"); | |
| None | |
| }) | |
| .map(|t| Arc::new(shared::tenderly_api::TenderlyCodeSimulator::new(t, chain.id()))); |
| let tenderly = args | ||
| .shared | ||
| .tenderly | ||
| .get_api_instance(&http_factory, "order_simulation".to_owned()) | ||
| .unwrap() | ||
| .map(|t| Arc::new(shared::tenderly_api::TenderlyCodeSimulator::new(t, chain_id))); |
There was a problem hiding this comment.
Using .unwrap() here will cause the service to panic on startup if the Tenderly API is misconfigured. Since Tenderly is used for logging and is not a critical component, it would be more robust to handle the error gracefully, log a warning, and continue without Tenderly simulation. This would prevent a misconfiguration in an optional component from taking down the entire service.
| let tenderly = args | |
| .shared | |
| .tenderly | |
| .get_api_instance(&http_factory, "order_simulation".to_owned()) | |
| .unwrap() | |
| .map(|t| Arc::new(shared::tenderly_api::TenderlyCodeSimulator::new(t, chain_id))); | |
| let tenderly = args.shared.tenderly.get_api_instance(&http_factory, "order_simulation".to_owned()) | |
| .unwrap_or_else(|err| { | |
| tracing::warn!(?err, "failed to initialize tenderly api"); | |
| None | |
| }) | |
| .map(|t| Arc::new(shared::tenderly_api::TenderlyCodeSimulator::new(t, chain_id))); |
|
This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed. |
MartinquaXD
left a comment
There was a problem hiding this comment.
Thank you for your contribution and sorry for the very long time without reviews. 🙇
A team member is currently also looking into this feature. We'll discuss how we can allocate resources here.
| } | ||
|
|
||
| pub struct OrderExecutionSimulator { | ||
| #[expect(dead_code)] |
There was a problem hiding this comment.
Why do we expect the dead code here?
| // We consider a quote to be verified if it comes from an account other | ||
| // than the zero address. | ||
| self.from != Address::ZERO | ||
| } |
There was a problem hiding this comment.
This doesn't seem to be used. Also quote verification is orthogonal to the from address. Even if a user does not connect their wallet in the frontend, which leads to from being 0, we are still able to verify quotes for the majority of relevant tokens.
| Ok(EncodedSettlement { | ||
| tokens, | ||
| clearing_prices, | ||
| trades: vec![trade], | ||
| interactions: Default::default(), | ||
| }) |
There was a problem hiding this comment.
This logic does not take features from the appdata into consideration (e.g. pre-, post-interactions, flashloans, generic wrappers). See here for how the driver encodes the settlements that ultimately get submitted.
The logic for applying fees by adjusting the price vector are not needed here as this simulation only concerns itself with the integrity of the smart contract parts - not with how achievable the limit price is.
There was a problem hiding this comment.
The component is ultimately supposed to be used in a few places:
- order placement endpoint
- custom debugging endpoint
- possibly in the autopilot and driver for filtering orders
Given this high visibility it seems like this component shouldn't be hidden so deep in the file tree.
There was a problem hiding this comment.
I'm going to address this one first.
I would just move it to crates/shared/src/order_simulation.rs.
Description
Implement unified order execution simulator infrastructure for end-to-end order validation (#4006).
This PR introduces
OrderExecutionSimulator. It allows simulation for order execution, to validate settlement contract state transitions, token transfers and allowances, EIP-1271 signature validity, pre/post-interaction hooks, wrapper and flashloan setup.I use
eth_callwith state overrides to avoid changing blockchain state, and logs failures to Tenderly for debugging.Changes
OrderExecutionSimulatingtrait for pluggable simulationOrderExecutionSimulatorwith settlement encoding and state override preparationOrderQuotervia factory patternFakeOrderExecutionSimulatorto forgive unit testingHow to test
Runs the E2E test suite. The simulator is integrated into the verified quote flow so it'll be exercised by any tests that request verified quotes.
Fixes #4006