Skip to content

[feature] implement unified order simulation logic#4127

Open
theghostmac wants to merge 10 commits intocowprotocol:mainfrom
theghostmac:feature/unified-order-simulation
Open

[feature] implement unified order simulation logic#4127
theghostmac wants to merge 10 commits intocowprotocol:mainfrom
theghostmac:feature/unified-order-simulation

Conversation

@theghostmac
Copy link

@theghostmac theghostmac commented Feb 4, 2026

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_call with state overrides to avoid changing blockchain state, and logs failures to Tenderly for debugging.

Changes

  • Add OrderExecutionSimulating trait for pluggable simulation
  • Implement OrderExecutionSimulator with settlement encoding and state override preparation
  • Integrate simulator into OrderQuoter via factory pattern
  • Add FakeOrderExecutionSimulator to forgive unit testing
  • Fix service initialization in orderbook and autopilot runs
  • Tenderly logging if the simulation fails.

How to test

just test-e2e quote_verification

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

- 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
@theghostmac theghostmac requested a review from a team as a code owner February 4, 2026 19:58
@theghostmac theghostmac marked this pull request as draft February 4, 2026 19:58
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

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.

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),
Copy link
Contributor

Choose a reason for hiding this comment

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

high

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.

Suggested change
fee_amount: U256::from(trade_estimate.gas),
fee_amount: fee_parameters.fee(),

Copy link
Author

Choose a reason for hiding this comment

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

good catch. addressed.

.shared_args
.tenderly
.get_api_instance(&self.components.http_factory, "order_simulation".to_owned())
.unwrap()
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Using .unwrap() on the result of get_api_instance can cause a panic if Tenderly is misconfigured (e.g., invalid API key format), leading to a service crash. It's safer to propagate the error using ? since the order_quoter function returns a Result.

Suggested change
.unwrap()
?

Copy link
Author

Choose a reason for hiding this comment

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

addressed.

Comment on lines 74 to 93
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

high

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
        };

Copy link
Author

Choose a reason for hiding this comment

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

addressed.

@theghostmac
Copy link
Author

theghostmac commented Feb 6, 2026

i was blocked on running integration tests for this, so i spent time reading through e2e/tests and discovered quote_verification.rs.

i realized i can just run just test-e2e quote_verification since it now implicitly uses my OrderExecutionSimulator.

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 verify_quote() already handles).

the local e2e test passes. the forked test requires FORK_URL_MAINNET which isn't set up for me.

FORK_URL_MAINNET must be set to run forked tests: NotPresent

@theghostmac theghostmac marked this pull request as ready for review February 6, 2026 02:02
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines 478 to 483
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())));
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Using .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.

Suggested change
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())));

Copy link
Author

Choose a reason for hiding this comment

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

addressed

Comment on lines 364 to 369
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)));
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Using .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.

Suggested change
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)));

Copy link
Author

Choose a reason for hiding this comment

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

addressed

@github-actions
Copy link

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.

@github-actions github-actions bot added the stale label Feb 14, 2026
Copy link
Contributor

@MartinquaXD MartinquaXD left a comment

Choose a reason for hiding this comment

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

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)]
Copy link
Contributor

Choose a reason for hiding this comment

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

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
}
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines 111 to 116
Ok(EncodedSettlement {
tokens,
clearing_prices,
trades: vec![trade],
interactions: Default::default(),
})
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Author

@theghostmac theghostmac Feb 17, 2026

Choose a reason for hiding this comment

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

I'm going to address this one first.
I would just move it to crates/shared/src/order_simulation.rs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unified order simulation logic

2 participants