Skip to content

Simulate EIP-1271 orders at creation#4366

Draft
squadgazzz wants to merge 63 commits intonew-api-simulator-cratefrom
prototype-eip1271-on-new-api
Draft

Simulate EIP-1271 orders at creation#4366
squadgazzz wants to merge 63 commits intonew-api-simulator-cratefrom
prototype-eip1271-on-new-api

Conversation

@squadgazzz
Copy link
Copy Markdown
Contributor

@squadgazzz squadgazzz commented Apr 29, 2026

Problem

Same as #4355: orderbook accepts an EIP-1271 order if the signer contract's isValidSignature says yes. That single check is enough to let through Aave flashloan-style orders where the signature passes but the post-hook can never settle. We catch these later, after they're in an auction, wasting solver cycles.

A symmetric class (Euler-style) is too strict on signatures but would actually settle fine. Those are rejected today.

Fix

This PR is the successor to #4355, rebuilt on top of new-api-simulator-crate (@MartinquaXD's SettlementSimulator refactor). At order-creation time, we run the signature check and a full-order simulation concurrently, then combine the results.

Per chain, a mode config picks what to do with the combined result:

  • disabled (default): same as today. Signature check only.
  • shadow: both run, disagreements land in metrics and logs, behaviour unchanged.
  • enforce: if signature passes but simulation fails, reject with HTTP 400 Eip1271SimulationFailed.

Infra errors (RPC, timeout, Tenderly) never reject in any mode. Signature-fail cases return the same InvalidEip1271Signature as before, regardless of the simulation result.

What changed vs #4355

  • The adapter drives SettlementSimulator::new_simulation_builder() instead of the legacy OrderSimulator.
  • parameters_from_app_data handles pre/post hooks, custom wrappers, and Aave flashloan configs uniformly from the user-signed app_data. The earlier prototype injected these manually.
  • The wrapper path (WrapperConfig::Flashloan(...)) routes through the deployed FlashLoanRouter, so the validation-time simulation now reflects the real Aave flashloan flow end-to-end. The borrower-to-trader transfer concern raised in the Simulate EIP-1271 orders at creation #4355 review is handled by the user-signed pre-hook (deploy helper, fund from factory), which parameters_from_app_data includes automatically.
  • orderbook::run passes the FlashLoanRouter deployment address to SettlementSimulator::new instead of Address::ZERO, which would otherwise silently no-op the wrapper call.

Rollout

Same as #4355. Land as disabled. Flip to shadow in prod to see the matrix on real traffic. Flip to enforce once we trust the sim.

Changes

  • shared::order_validation: new Eip1271Simulating trait, Eip1271Simulator bundle (simulator + mode + timeout) threaded into OrderValidator. Validation runs signature + sim concurrently via tokio::join! with a per-call timeout, emits an eip1271_simulation_total{signature, simulation} Prometheus matrix, upgrades enforce-mode disagreements to ValidationError::SimulationFailed.
  • orderbook::eip1271_simulation: OrderSimulatorAdapter driving SettlementSimulator::new_simulation_builder() + parameters_from_app_data.
  • orderbook::run: wire the deployed FlashLoanRouter address.
  • configs: Eip1271SimulationMode enum with disabled, shadow, enforce variants.

How to test

Unit tests in shared::order_validation cover the signature × simulation matrix, enforce-mode rejection, fail-open on infra errors, the eip1271_skip_creation_validation path, and the no-simulator-configured path. configs tests deserialize the three modes.

Local-node e2e in crates/e2e/tests/e2e/eip1271_creation_simulation.rs:

  • Negative: a Safe-signed order whose app_data.protocol.wrappers points at a custom always-revert wrapper. With Eip1271SimulationMode::Enforce, the orderbook returns HTTP 400 Eip1271SimulationFailed.
  • Positive: a Safe-signed order with empty app_data is accepted.

Forked-mainnet replay in crates/simulator/tests/aave_replay.rs (gated on MAINNET_RPC_URL, otherwise skipped).

A full e2e test against a forked node isn't viable: OrderValidator::partial_validate checks valid_to against SystemTime::now(), and any historical order's valid_to is in the past on a fresh test run, so the orderbook rejects with InsufficientValidTo before the simulation ever runs. These tests therefore bypass the validator and drive SettlementSimulator directly.

  • Replay of a real Aave v3 debt-swap order pinned to the block right before settlement. Asserts the simulation succeeds.
  • Same order with flashloan.amount rewritten past Aave's WETH liquidity. Asserts the simulation reverts with execution reverted. The eth_call goes to FlashLoanRouter, which forwards to AaveBorrower, which asks the Aave Pool for the loan. Aave is what reverts (insufficient liquidity), but the revert only reaches us because the router and borrower addresses are correctly wired. If the wrapper had no-op'd against Address::ZERO the simulation would have succeeded silently.

Runs the OrderSimulator concurrently with the cheap isValidSignature
check. In shadow mode (default), logs disagreements via metrics and
structured logs but returns the cheap check's result. In enforce mode,
(cheap Pass, sim Fail) is upgraded to ValidationError::SimulationFailed;
other combinations stay unchanged. Infra errors never reject.

Covers scope from plan Tasks 3, 4 and 5: shadow-mode quadrants,
enforce-mode cases, infra/skip-flag/no-sim paths.
The Shadow/Enforce distinction is a mode variant, not a property of the
capability — the same simulator infrastructure is active in both modes.
Keeping "shadow" only where it names a mode variant, and renaming the
trait, error, mode enum, metrics, constant, config fields, and module
accordingly.
Doc comments and local variable names still described the capability
as 'shadow' — rewritten to reference the mode variant only where it
genuinely applies (config docs, test names that exercise Shadow mode,
the Shadow enum variant).
The simulator, mode, and timeout are only meaningful together. Collapsing
them into a single Option<Eip1271SimConfig> field lets the call site in
run.rs return None cleanly when order_simulation isn't configured,
instead of passing placeholder mode/timeout values that aren't read.
… to Simulator

Matches the project's convention of -ing suffix for traits
(OrderValidating/OrderValidator, SignatureValidating, BalanceFetching).
The former Eip1271SimConfig bundle becomes the concrete Eip1271Simulator
struct, and the trait it depends on becomes Eip1271Simulating.
The helper now accepts the full simulator bundle instead of three
separate (simulator, mode, timeout) args. Introduces shadow_mode_sim /
enforce_mode_sim helpers to keep test call sites tight.
The constant is only used by tests; keeping it at module scope (and
public) implied an API contract with the configs crate that doesn't
exist. configs::orderbook::default_eip1271_sim_timeout is authoritative
at runtime.
The existing EIP-1271 check is an isValidSignature call; 'cheap' was
editorializing on cost rather than describing what it does. Renaming
to 'signature' for the metric axis, outcome enum, and supporting names.

Also collapsing sim_only_total into total by adding a 'skipped' value
to the signature axis — one counter with a signature × sim matrix
covers every case the two used to cover.
…configs, logs

The Eip1271Simulator struct keeps its -Simulator suffix (matching
OrderValidator/-Validating), but everywhere sim was used as a modifier
or qualifier (enum variants, error type, metric subsystem/labels,
config fields, log messages, test names) it now reads as simulation.
Operators can now opt out of the simulation at order creation on a
per-chain basis without giving up the /debug/simulation endpoint. The
shared mode enum stays binary (Shadow/Enforce); Disabled translates to
None for OrderValidator at the wiring layer. The OrderSimulator is
still constructed for the debug endpoint.

Also removed the redundant impls_trait compile-check test in
eip1271_simulation.rs — the impl block above already enforces that at
compile time.
Mock both the signature validator and the simulator with times(0) and
submit an Eip712 (EOA) order. Catches a regression where the sim is
accidentally wired to run for non-1271 orders.
The seven near-identical tests covering every (signature, simulation,
mode) combination collapsed into one table-driven test with a single
mock-driven helper. Failure messages include a label per row so any
regression still pinpoints the failing cell.

Also inlined the now-unused enforce_mode_simulator helper and replaced
shadow_mode_simulator with a general simulator_with_mode.
- Move simulation_time histogram timer inside simulation_fut so it no
  longer captures the max of sig-check + simulation latency.
- Warn-level (not info) for simulation failures in the
  eip1271_skip_creation_validation path, matching the normal path.
- Drop Default derive on the shared Eip1271SimulationMode since the
  operational default lives in configs (Disabled) and no code reaches
  for it. Updated the doc to clarify the split.
- New configs test asserting "shadow" deserializes to the Shadow
  variant.
@squadgazzz squadgazzz changed the base branch from main to new-api-simulator-crate April 29, 2026 11:20
@squadgazzz squadgazzz force-pushed the prototype-eip1271-on-new-api branch from f960037 to 5940c11 Compare April 29, 2026 14:38
@squadgazzz squadgazzz force-pushed the prototype-eip1271-on-new-api branch from 5940c11 to b162fb4 Compare April 29, 2026 14:47
@squadgazzz squadgazzz changed the title Prototype eip1271 on new api Simulate EIP-1271 orders at creation Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant