Skip to content

Commit 76446ea

Browse files
m-szjmg-duartesquadgazzz
authored
Add block number to order simulation (#4305)
# Description The order simulation endpoint would always run against the latest block. It is limiting for investigating already filled or partially filled orders. # Changes Add query parameter to the order simulation endpoint to allow specifying a block number ## How to test E2E test creates an order, withdraws trader's sell tokens on block A and funds the account on block B. The order simulation fails for block A and succeeds for block B. --------- Co-authored-by: José Duarte <duarte.gmj@gmail.com> Co-authored-by: ilya <ilya@cow.fi>
1 parent 5b22ece commit 76446ea

8 files changed

Lines changed: 165 additions & 9 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/e2e/tests/e2e/order_simulation.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use {
2+
alloy::{primitives::Address, providers::Provider},
23
configs::test_util::TestDefault,
34
e2e::setup::{API_HOST, OnchainComponents, Services, run_test},
45
ethrpc::{Web3, alloy::CallBuilderExt},
@@ -18,6 +19,12 @@ async fn local_node_order_simulation() {
1819
run_test(order_simulation).await;
1920
}
2021

22+
#[tokio::test]
23+
#[ignore]
24+
async fn local_node_order_simulation_block_number() {
25+
run_test(order_simulation_block_number).await;
26+
}
27+
2128
async fn order_simulation(web3: Web3) {
2229
let mut onchain = OnchainComponents::deploy(web3.clone()).await;
2330

@@ -87,3 +94,115 @@ async fn order_simulation(web3: Web3) {
8794
assert_eq!(tenderly.simulation_type, Some(SimulationType::Full));
8895
assert_eq!(tenderly.value, None);
8996
}
97+
98+
async fn order_simulation_block_number(web3: Web3) {
99+
let mut onchain = OnchainComponents::deploy(web3.clone()).await;
100+
101+
let [solver] = onchain.make_solvers(10u64.eth()).await;
102+
let [trader] = onchain.make_accounts(10u64.eth()).await;
103+
let [token] = onchain
104+
.deploy_tokens_with_weth_uni_v2_pools(1_000u64.eth(), 1_000u64.eth())
105+
.await;
106+
107+
// Fund trader so the order passes balance validation at submission time.
108+
onchain
109+
.contracts()
110+
.weth
111+
.deposit()
112+
.from(trader.address())
113+
.value(3u64.eth())
114+
.send_and_watch()
115+
.await
116+
.unwrap();
117+
onchain
118+
.contracts()
119+
.weth
120+
.approve(onchain.contracts().allowance, 3u64.eth())
121+
.from(trader.address())
122+
.send_and_watch()
123+
.await
124+
.unwrap();
125+
126+
let services = Services::new(&onchain).await;
127+
services
128+
.start_protocol_with_args(
129+
configs::autopilot::Configuration::test("test_solver", solver.address()),
130+
configs::orderbook::Configuration::test_default(),
131+
solver,
132+
)
133+
.await;
134+
135+
let order = OrderCreation {
136+
sell_token: *onchain.contracts().weth.address(),
137+
sell_amount: 2u64.eth(),
138+
buy_token: *token.address(),
139+
buy_amount: 1u64.eth(),
140+
valid_to: model::time::now_in_epoch_seconds() + 300,
141+
kind: OrderKind::Buy,
142+
..Default::default()
143+
}
144+
.sign(
145+
EcdsaSigningScheme::Eip712,
146+
&onchain.contracts().domain_separator,
147+
&trader.signer,
148+
);
149+
let uid = services.create_order(&order).await.unwrap();
150+
151+
// Transfer all WETH away from the trader — now they have no sell-token
152+
// balance. The current block becomes the "no funds" snapshot.
153+
let burn = Address::from([0x42u8; 20]);
154+
onchain
155+
.contracts()
156+
.weth
157+
.transfer(burn, 3u64.eth())
158+
.from(trader.address())
159+
.send_and_watch()
160+
.await
161+
.unwrap();
162+
let block_no_funds = web3.provider.get_block_number().await.unwrap();
163+
164+
// Re-deposit WETH. The current block now has the trader fully funded again.
165+
onchain
166+
.contracts()
167+
.weth
168+
.deposit()
169+
.from(trader.address())
170+
.value(3u64.eth())
171+
.send_and_watch()
172+
.await
173+
.unwrap();
174+
let block_with_funds = web3.provider.get_block_number().await.unwrap();
175+
176+
let client = services.client();
177+
178+
// Simulation at the block where the trader had no WETH must fail.
179+
let response = client
180+
.get(format!(
181+
"{API_HOST}/api/v1/debug/simulation/{uid}?block_number={block_no_funds}"
182+
))
183+
.send()
184+
.await
185+
.unwrap();
186+
assert_eq!(response.status(), StatusCode::OK);
187+
let result = response.json::<OrderSimulationResult>().await.unwrap();
188+
assert!(
189+
result.error.is_some(),
190+
"expected simulation failure at block {block_no_funds} (no funds), got success"
191+
);
192+
193+
// Simulation at the block where the trader has WETH must succeed.
194+
let response = client
195+
.get(format!(
196+
"{API_HOST}/api/v1/debug/simulation/{uid}?block_number={block_with_funds}"
197+
))
198+
.send()
199+
.await
200+
.unwrap();
201+
assert_eq!(response.status(), StatusCode::OK);
202+
let result = response.json::<OrderSimulationResult>().await.unwrap();
203+
assert_eq!(
204+
result.error, None,
205+
"expected simulation success at block {block_with_funds} (funded), got error: {:?}",
206+
result.error
207+
);
208+
}

crates/orderbook/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ configs = { workspace = true }
3333
const-hex = { workspace = true }
3434
contracts = { workspace = true }
3535
database = { workspace = true }
36+
eth-domain-types = { workspace = true }
3637
ethrpc = { workspace = true }
3738
futures = { workspace = true }
3839
gas-price-estimation = { workspace = true }

crates/orderbook/openapi.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,15 @@ paths:
778778
schema:
779779
$ref: "#/components/schemas/UID"
780780
required: true
781+
- in: query
782+
name: block_number
783+
schema:
784+
type: integer
785+
format: int64
786+
required: false
787+
description: >
788+
Block number to simulate the order at. If not specified, the
789+
simulation uses the latest block.
781790
responses:
782791
"200":
783792
description: Simulation request returned.

crates/orderbook/src/api/debug_simulation.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
use {
22
crate::{api::AppState, orderbook::OrderSimulationError},
33
axum::{
4-
extract::{Path, State},
4+
extract::{Path, Query, State},
55
http::StatusCode,
66
response::{IntoResponse, Json, Response},
77
},
88
model::order::OrderUid,
9+
serde::Deserialize,
910
std::sync::Arc,
1011
};
1112

13+
#[derive(Deserialize)]
14+
pub struct SimulationQuery {
15+
pub block_number: Option<u64>,
16+
}
17+
1218
pub async fn debug_simulation_handler(
1319
State(state): State<Arc<AppState>>,
1420
Path(uid): Path<OrderUid>,
21+
Query(params): Query<SimulationQuery>,
1522
) -> Response {
16-
match state.orderbook.simulate_order(&uid).await {
23+
match state
24+
.orderbook
25+
.simulate_order(&uid, params.block_number)
26+
.await
27+
{
1728
Ok(Some(result)) => (StatusCode::OK, Json(result)).into_response(),
1829
Ok(None) => (
1930
StatusCode::NOT_FOUND,

crates/orderbook/src/order_simulator.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use {
77
anyhow::{Context, Result},
88
balance_overrides::BalanceOverrideRequest,
99
contracts::alloy::support::{AnyoneAuthenticator, Trader},
10+
eth_domain_types::BlockNo,
1011
model::order::Order,
1112
simulator::{
1213
encoding::InteractionEncoding,
@@ -81,8 +82,17 @@ impl OrderSimulator {
8182
/// The result contains the transaction simulation error (if any)
8283
/// and a full API request object that can be used to resimulate the swap
8384
/// using Tenderly.
84-
pub async fn simulate_swap(&self, swap: EncodedSwap) -> Result<OrderSimulationResult> {
85-
let result = self.simulator.simulate_settle_call(swap).await?;
85+
pub async fn simulate_swap(
86+
&self,
87+
swap: EncodedSwap,
88+
block_number: Option<u64>,
89+
) -> Result<OrderSimulationResult> {
90+
let block_number =
91+
block_number.unwrap_or_else(|| self.simulator.current_block.borrow().number);
92+
let result = self
93+
.simulator
94+
.simulate_settle_call(swap, Some(block_number))
95+
.await?;
8696

8797
let tenderly_request = simulator::tenderly::dto::Request {
8898
transaction_index: None,
@@ -92,7 +102,7 @@ impl OrderSimulator {
92102
self.chain_id.clone(),
93103
&result.tx,
94104
result.overrides,
95-
None,
105+
Some(BlockNo(block_number)),
96106
)?
97107
};
98108

crates/orderbook/src/orderbook.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@ impl Orderbook {
616616
pub async fn simulate_order(
617617
&self,
618618
uid: &OrderUid,
619+
block_number: Option<u64>,
619620
) -> Result<Option<OrderSimulationResult>, OrderSimulationError> {
620621
let Some(order_simulator) = &self.order_simulator else {
621622
return Err(OrderSimulationError::NotEnabled);
@@ -634,7 +635,7 @@ impl Orderbook {
634635
.map_err(OrderSimulationError::Other)?;
635636
Ok(Some(
636637
order_simulator
637-
.simulate_swap(swap)
638+
.simulate_swap(swap, block_number)
638639
.await
639640
.map_err(OrderSimulationError::Other)?,
640641
))

crates/simulator/src/swap_simulator.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,12 @@ impl SwapSimulator {
235235
})
236236
}
237237

238-
pub async fn simulate_settle_call(&self, swap: EncodedSwap) -> Result<SwapSimulation<Bytes>> {
239-
let block = *self.current_block.borrow();
238+
pub async fn simulate_settle_call(
239+
&self,
240+
swap: EncodedSwap,
241+
block_number: Option<u64>,
242+
) -> Result<SwapSimulation<Bytes>> {
243+
let block_number = block_number.unwrap_or_else(|| self.current_block.borrow().number);
240244
let (settlement_target, calldata) = self.get_target_and_calldata(&swap);
241245

242246
let overrides = swap.overrides;
@@ -253,7 +257,7 @@ impl SwapSimulator {
253257
.provider
254258
.call(tx.clone())
255259
.overrides(overrides.clone())
256-
.block(block.number.into())
260+
.block(block_number.into())
257261
.await
258262
.map_err(|err| anyhow!(err));
259263

0 commit comments

Comments
 (0)