Compare Uniswap V3 and V4 routes
Compare Uniswap V3, V4, and split ETH → USDC routes at one pinned block. Simulate family leaders, rank after gas, and inspect every check and rejection reason.
Goal #
Compare Uniswap V3, V4, and split ETH → USDC routes at one pinned Ethereum block.
The notebook keeps routes as rows from quote through decision. It quotes the full set, simulates the leading V3, V4, and split routes as exact Universal Router calls, then ranks only calls that execute successfully and pass every check. Failed routes stay in the record with their rejection reasons.
Simulation only. Nothing is signed or sent.
Explore or rerun the route decision #
The guide opens with dated results for the default inputs. Change the trade size or sender, then select Run all to replace them with a fresh run against a finalized Ethereum mainnet block.
1. Create the market checkpoint #
Start by pinning the client to a finalized Ethereum mainnet block. Every contract read, quote, and simulation uses that state.
The checkpoint also records the candidate pools, trade inputs, sender, router deadline, and the values used to price gas. get_gas_price samples the RPC gas price when the notebook runs; it is not part of the pinned state. The ETH/USD value comes from Chainlink at the pinned block. The notebook accepts it only if the oracle answer is valid and no more than two hours old, then treats 1 USDC = 1 USD when converting gas cost to USDC.
2. Verify runtime-code identity #
Before quoting any route, the notebook checks every declared dependency and discovered V3 pool against its expected runtime-code hash. A missing pool or hash mismatch blocks quoting, so the notebook cannot return PASS.
These checks confirm runtime-code identity at the pinned block. They do not establish that the contracts are secure or correct.
3. Compare route quotes #
At the pinned block, quote every direct V3 and V4 route and every 50/50 split between the declared V3 and V4 pools. Rank them by quoted USDC output before gas. The top quote in each family advances to exact simulation, but a quote leader is not yet a winner.
Every quote stays in the final comparison. An unsimulated route can still block PASS, so a simulated route does not win simply because it was one of the three routes executed.
4. Simulate exact router calls #
Quotes and swaps use different calls, so a leading quote is not enough. The notebook encodes the top V3, V4, and split routes as exact Universal Router calls, then simulates all three against the same pinned state.
It keeps the calldata, pinned block, gas estimate, and sender and router balances before and after execution for later checks. Each call allows output up to 100 bps below its quote. Because the quote and simulation use the same state, a later check requires the simulated output to match the quote exactly.
5. Rank routes after gas #
Only simulations that complete successfully and pass every route check enter the after-gas ranking. The simulated output must match the quote exactly. Balance observations must be complete, the call tree may reach only expected targets and contexts, and the router’s ETH, WETH, and USDC balances must not change. For each eligible route, the notebook converts the gas estimate to USDC and subtracts it from the simulated output.
The notebook reprices the same gas estimate at 0.5×, 1×, and 2× the sampled gas price to show whether the route order changes. Only the 1× ranking controls the final decision. Rejected routes stay visible with their first failed check.
6. Make the route decision #
preflight_decision is the final result. It can return PASS only if the market and runtime-code checks pass, every generated route has a complete quote, simulation evidence is complete for the leading V3, V4, and split routes, and at least one simulated route remains eligible after the execution checks.
The final comparison gives unsimulated routes the benefit of the doubt. It uses the highest pre-gas quote among them as the bar. The leading eligible route must match or beat that quote after its own gas cost. Otherwise, the result is FAIL.
A PASS applies only to the routes in this guide, these inputs, this pinned block, and the sampled gas price. It does not claim the best route outside this list or at a later block.
7. Inspect decision checks #
decision_check_summary is the audit view for preflight_decision. It keeps rejected routes visible and shows each route or global check with its observed value, expected value, and rejection reason.
Treat preflight_decision as the final result. This table explains that result, but it does not expose every completeness count used to calculate it.
8. Inspect the selected call path #
After PASS, inspect the selected route’s call tree instead of stopping at its final output. Each row identifies the code address reached, its call type and context, and its position in the tree.
UNEXPECTED_TARGET marks code outside the allowed route targets. The first row also includes the retained execution record for further queries.
Query the decision record #
The winning route is only one row in the result. The notebook keeps the pinned block, full route set, encoded calls, simulation results, checks, and rejection reasons together as queryable tables.
Change the route set or ranking rule and run the notebook again. You can inspect which route ranks first after gas and whether its exact router call matched expectations. Call frames and balance changes are already available as tables, so you can query them without first building a custom trace parser.
The decision applies only to the routes, inputs, pinned block, and sampled gas price used here. execution_result_root covers the simulated execution only, not the quotes or ranking SQL. Nothing is signed or sent.
Functions used in this guide
- get_block
- get_gas_price
- client_context
- pin
- read_contract
- read_contract_multicall
- native_balance
- code_at
- parse_units
- parse_ether
- format_units
- format_ether
- muldiv
- evm_ceil_div
- try_mul
- try_sub
- keccak256
- abi_encode_packed
- encode_function_data
- encode_function_args
- call_decode
- to_timestamp
- client
- program
- execute_call
- assume_no_code
- assume_native_balance
- observation_fact
- observe
- run