Compare Uniswap V3 and V4 routes

Inspect a Uniswap route decision in Determica, from pinned chain state and exact calldata to execution evidence and every check behind PASS or FAIL.

Goal #

A quote gives you a number. It does not show whether the exact transaction reproduces that number against the same state, which code and call contexts it reaches, what balance and final effects survive, how gas changes the ranking, or why a candidate was rejected.

This guide keeps the route decision in one editable Determica notebook. Candidates remain inspectable as data. Contract state is pinned. Selected routes execute as exact calldata. Receipts, observations, effects, checks, rankings, failure reasons, and the selected call trace are published as queryable tables.

Run the route decision #

Set the trade size and sender, then run all eight steps against finalized Ethereum mainnet state. The notebook publishes evidence as queryable tables. It does not sign or send a transaction.

Inputs

1. Create the market checkpoint #

Create one market checkpoint for all later quotes, simulations, and checks. Pin the client to a finalized Ethereum mainnet block by default. Record the candidate pools, trade inputs, sender, and router deadline. get_gas_price samples the RPC gas price when the notebook runs, while contract reads use the pinned block. Use the pinned Chainlink ETH/USD price to estimate gas cost in USDC. The estimate requires a valid oracle answer no more than two hours old and assumes 1 USDC = 1 USD.

1
CREATE OR REPLACE TABLE ticket AS
2
WITH base AS (
3
SELECT
4
b.number AS block_number,
5
b.hash AS block_hash,
6
b.timestamp AS block_timestamp,
7
parse_ether(CAST($amount_in_eth AS VARCHAR)) AS amount_in_raw,
8
CAST($max_slippage_bps AS UBIGINT) AS slippage_bps,
9
get_gas_price($client) AS gas_price_wei,
10
CAST($research_sender_address AS ADDRESS) AS sender,
11
(client_context(p.client)).chain_id::UBIGINT AS chain_id,
12
-- Set the Universal Router deadline 15 minutes after the pinned block.
13
(epoch(b.timestamp)::UBIGINT + 900)::UINT256 AS deadline,
14
p.client,
15
'0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af'::ADDRESS AS router,
16
'0x000000000004444c5dc75cB358380D2e3dE08A90'::ADDRESS AS pool_manager,
17
'0x52f0E24D1c21C8A0cB1e5a5dD6198556BD9E1203'::ADDRESS AS v4_quoter,
18
'0x61fFE014bA17989E743c5F6cB21bF9697530B21e'::ADDRESS AS v3_quoter,
19
'0x1F98431c8aD98523631AE4a59f267346ea31F984'::ADDRESS AS v3_factory,
20
'0xcA11bde05977b3631167028862bE2a173976CA11'::ADDRESS AS multicall3,
21
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'::ADDRESS AS usdc,
22
'0x43506849D7C04F9138D1A2050bbF3A0c054402dd'::ADDRESS AS usdc_implementation,
23
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'::ADDRESS AS weth,
24
'0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419'::ADDRESS AS eth_usd_feed,
25
'0x0000000000000000000000000000000000000000'::ADDRESS AS native_eth,
26
'0x0000000000000000000000000000000000000000'::ADDRESS AS zero_hook
27
FROM get_block($client, $block_selector) b
28
CROSS JOIN LATERAL (
29
SELECT pin($client, b.number::UBIGINT, b.hash, true) AS client
30
) p
31
)
32
SELECT
33
base.*,
34
-- Require 2 wei so each 50/50 split leg receives input.
35
base.amount_in_raw >= 2::UINT256
36
-- Keep V4 quote and swap amounts within uint128.
37
AND base.amount_in_raw <= CAST(
38
'340282366920938463463374607431768211455' AS UINT256
39
)
40
-- Limit retained-output slippage to 100%.
41
AND base.slippage_bps <= 10000 AS route_parameters_ok,
42
try_sub(10000::UINT256, base.slippage_bps::UINT256)
43
AS retained_slippage_bps,
44
CASE
45
WHEN code_at(client, sender) = '0x'::BYTES THEN 'SUPPORTED_EOA'
46
ELSE 'UNSUPPORTED_CONTRACT_OR_DELEGATED_SENDER'
47
END AS sender_status
48
FROM base;
49
 
50
CREATE OR REPLACE TABLE pools AS
51
-- 157 more lines load when JavaScript runs

2. Verify runtime-code identity #

Before comparing routes, confirm that each declared dependency and discovered V3 pool matches its expected runtime code. A missing pool or hash mismatch stops the quote step and prevents PASS.

These checks establish code identity at the pinned block. They do not establish contract security or correctness.

1
CREATE OR REPLACE TABLE approved_code_dependencies AS
2
WITH manifest AS (
3
SELECT m.*
4
FROM ticket t
5
CROSS JOIN LATERAL (
6
VALUES
7
(
8
'router'::VARCHAR,
9
t.router,
10
'0x6a5f46971b50c6e1b7eef97902311444e479d734e4f80ad88367783cf373fe7f'::BYTES32
11
),
12
(
13
'pool_manager',
14
t.pool_manager,
15
'0x785f1014552b7ce7d5fb7d0c970ca60edee94fd00425d7ca21609acac7ce1293'::BYTES32
16
),
17
(
18
'weth',
19
t.weth,
20
'0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23'::BYTES32
21
),
22
(
23
'usdc_proxy',
24
t.usdc,
25
'0xd80d4b7c890cb9d6a4893e6b52bc34b56b25335cb13716e0d1d31383e6b41505'::BYTES32
26
),
27
(
28
'usdc_implementation',
29
t.usdc_implementation,
30
'0xcdfb7d322961af3acae7a8f7ee8b69c205b36f576cc5b077f170c7eb8ecbe3ea'::BYTES32
31
),
32
(
33
'v3_quoter',
34
t.v3_quoter,
35
'0x06148f47d0f41a68d3bc970030a7150e5d608cfbc28d372440a2e41ce543d92b'::BYTES32
36
),
37
(
38
'v4_quoter',
39
t.v4_quoter,
40
'0x06de58fa119c5deaa7a667fb92d3894e25d9160e62fb82c8d86d43b47eefe441'::BYTES32
41
),
42
(
43
'v3_factory',
44
t.v3_factory,
45
'0x4d7b8525cd5d14343fa67a732fba5b24cddba11620ca88392f4ec6c52f91fd69'::BYTES32
46
),
47
(
48
'multicall3',
49
t.multicall3,
50
'0xd5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891'::BYTES32
51
-- 54 more lines load when JavaScript runs

3. Compare route quotes #

The quote leaderboard compares gross USDC output across the declared route set at the same pinned block. Gross output excludes gas, so the quote leader is not yet the decision winner.

The notebook carries the highest gross quote from each family into execution. It retains every other quote and later uses the highest unexecuted quote as a conservative comparison bound.

1
SELECT
2
CASE
3
WHEN discovered_v3_pool_count <> 3
4
THEN error('V3_POOL_DISCOVERY_INCOMPLETE')
5
WHEN code_manifest_ok IS NOT TRUE
6
THEN error('APPROVED_CODE_MISMATCH')
7
ELSE true
8
END AS trust_checkpoint_ok
9
FROM code_manifest_status;
10
 
11
CREATE OR REPLACE TABLE quote_surface AS
12
WITH routes AS (
13
SELECT
14
c.*,
15
a.v3_amount_in_raw,
16
t.amount_in_raw - a.v3_amount_in_raw AS v4_amount_in_raw,
17
((c.v3_share_pct > 0)::UINTEGER
18
+ (c.v4_share_pct > 0)::UINTEGER) AS expected_legs
19
FROM (
20
SELECT
21
pool_key AS route,
22
protocol,
23
CASE
24
WHEN protocol = 'v3' THEN pool_key
25
END AS v3_pool_key,
26
CASE
27
WHEN protocol = 'v4' THEN pool_key
28
END AS v4_pool_key,
29
CASE
30
WHEN protocol = 'v3' THEN 100
31
ELSE 0
32
END::UINTEGER AS v3_share_pct,
33
CASE
34
WHEN protocol = 'v4' THEN 100
35
ELSE 0
36
END::UINTEGER AS v4_share_pct
37
FROM pools
38
 
39
UNION ALL
40
 
41
SELECT
42
'split_' || v3.fee || '_' || v4.fee,
43
'split',
44
v3.pool_key,
45
v4.pool_key,
46
50::UINTEGER,
47
50::UINTEGER
48
FROM pools v3
49
CROSS JOIN pools v4
50
WHERE v3.protocol = 'v3'
51
-- 217 more lines load when JavaScript runs

4. Simulate exact router calls #

A quote does not establish that the corresponding router call produces the same output. This step encodes the three family leaders as exact Universal Router calls and executes them against the pinned state.

Each receipt binds a candidate to its payload hash, execution identity, and pinned block. Pre-execution and terminal balance observations record what changed. The notebook does not sign or send a transaction.

1
CREATE OR REPLACE TABLE route_programs AS
2
WITH inputs AS (
3
SELECT
4
q.*,
5
muldiv(
6
q.v3_amount_out_raw,
7
t.retained_slippage_bps,
8
10000::UINT256
9
) AS v3_min_raw,
10
muldiv(
11
q.v4_amount_out_raw,
12
t.retained_slippage_bps,
13
10000::UINT256
14
) AS v4_min_raw
15
FROM quote_surface q
16
CROSS JOIN ticket t
17
WHERE q.quote_status = 'QUOTE_OK'
18
AND t.sender_status = 'SUPPORTED_EOA'
19
AND t.route_parameters_ok
20
QUALIFY row_number() OVER (
21
PARTITION BY protocol
22
ORDER BY quoted_output_raw DESC NULLS LAST, route
23
) = 1
24
),
25
payloads AS (
26
SELECT
27
i.*,
28
v3_pool.pool_address AS v3_pool_address,
29
v3_pool.fee AS v3_pool_fee,
30
i.v3_min_raw + i.v4_min_raw AS min_output_raw,
31
-- Add the execute(...) selector to the router calldata.
32
encode_function_data(
33
$universal_router_codec_abi,
34
'execute',
35
CASE i.protocol
36
-- Use 0x0b WRAP_ETH, 0x00 V3_SWAP_EXACT_IN, and 0x10 V4_SWAP.
37
WHEN 'v3' THEN '0x0b00'::BYTES
38
WHEN 'v4' THEN '0x10'::BYTES
39
ELSE '0x0b0010'::BYTES
40
END,
41
CASE i.protocol
42
WHEN 'v3' THEN ARRAY[v3.wrap_input, v3.swap_input]
43
WHEN 'v4' THEN ARRAY[v4.v4_input]
44
ELSE ARRAY[v3.wrap_input, v3.swap_input, v4.v4_input]
45
END,
46
t.deadline
47
)::BYTES AS calldata
48
FROM inputs i
49
LEFT JOIN v3_pools v3_pool ON v3_pool.pool_key = i.v3_pool_key
50
LEFT JOIN pools v4_pool ON v4_pool.pool_key = i.v4_pool_key
51
-- 195 more lines load when JavaScript runs

5. Rank eligible execution results #

The execution leaderboard first checks whether each call passes every execution, evidence, and call-policy check. Net output is simulated USDC output minus gas valued in USDC. Rejected routes remain visible with their first failed check.

The 0.5×, 1×, and 2× scenarios show whether route order depends on the sampled gas price. Only the 1× ranking controls the decision.

1
CREATE OR REPLACE TABLE expected_execution_targets AS
2
SELECT
3
candidate_key,
4
name,
5
address,
6
expected_kind,
7
expected_context
8
FROM route_programs p
9
LEFT JOIN v3_pools vp ON vp.pool_key = p.v3_pool_key
10
CROSS JOIN ticket t
11
CROSS JOIN LATERAL (
12
VALUES
13
(
14
'router'::VARCHAR,
15
t.router,
16
true,
17
'call'::VARCHAR,
18
t.router
19
),
20
(
21
'weth',
22
t.weth,
23
p.protocol IN ('v3', 'split'),
24
'call',
25
t.weth
26
),
27
(
28
'v3_pool',
29
vp.pool_address,
30
p.protocol IN ('v3', 'split'),
31
'call',
32
vp.pool_address
33
),
34
(
35
'pool_manager',
36
t.pool_manager,
37
p.protocol IN ('v4', 'split'),
38
'call',
39
t.pool_manager
40
),
41
(
42
'usdc_proxy',
43
t.usdc,
44
true,
45
'call',
46
t.usdc
47
),
48
(
49
'usdc_implementation',
50
t.usdc_implementation,
51
-- 455 more lines load when JavaScript runs

6. Make the route decision #

preflight_decision is the authoritative result for the notebook run. PASS requires complete market, runtime-code, quote, execution, and evidence coverage.

The conservative winner bound compares the leading eligible route’s 1× net output with the highest gross quote among unexecuted routes. A PASS identifies a winner only within the declared route set and pinned state. It does not establish a global optimum or later-state validity.

1
CREATE OR REPLACE TABLE selected_route AS
2
SELECT
3
r.*,
4
r.net_output_raw >= r.max_unexecuted_gross_quote_raw AS winner_proven,
5
format_units(r.net_output_raw, 6) AS net_output_usdc,
6
format_units(r.max_unexecuted_gross_quote_raw, 6) AS max_unexecuted_gross_quote_usdc,
7
format_units(
8
r.net_output_raw::INT256 - r.max_unexecuted_gross_quote_raw::INT256,
9
6
10
) AS certificate_margin_usdc
11
FROM route_rankings r
12
WHERE r.scenario = '1x'
13
AND r.scenario_rank = 1;
14
 
15
CREATE OR REPLACE TABLE decision AS
16
WITH slate AS (
17
SELECT
18
(SELECT count(*) FROM quote_surface)::UINTEGER AS quote_candidate_count,
19
(SELECT count(*) FROM quote_surface WHERE quote_status = 'QUOTE_OK')::UINTEGER
20
AS quoted_candidate_count,
21
(SELECT count(*) FROM route_programs)::UINTEGER AS program_count,
22
(SELECT count(*) FROM executions)::UINTEGER AS execution_count,
23
(SELECT count(DISTINCT candidate_key) FROM executions)::UINTEGER
24
AS distinct_execution_count,
25
(SELECT count(*) FROM route_evidence)::UINTEGER AS evidence_count,
26
(
27
SELECT count(*)
28
FROM route_evidence
29
WHERE checks['observations_ok'] IS TRUE
30
AND checks['gas_ok'] IS TRUE
31
AND checks['execution_facts_ok'] IS TRUE
32
AND checks['execution_context_ok'] IS TRUE
33
AND checks['transaction_binding_ok'] IS TRUE
34
AND call_rows IS NOT NULL
35
)::UINTEGER AS complete_evidence_count,
36
(SELECT count(*) FROM route_rankings)::UINTEGER AS ranking_count,
37
(SELECT count(*) FROM route_rankings WHERE scenario = '1x')::UINTEGER
38
AS one_x_ranking_count,
39
(
40
SELECT count(*)
41
FROM expected_execution_targets x
42
LEFT JOIN approved_code_dependencies a ON a.address = x.address
43
WHERE a.address IS NULL
44
)::UINTEGER AS missing_code_coverage_count
45
)
46
SELECT
47
CASE
48
WHEN f.failure_reason IS NULL THEN 'PASS'
49
ELSE 'FAIL'
50
END AS outcome,
51
-- 84 more lines load when JavaScript runs

7. Inspect decision checks #

Use the audit table to determine why a route passed or failed. It expands each route’s ordered checks and the global comparison checks. Rejected routes remain visible with observed values and rejection reasons.

Use preflight_decision as the authoritative outcome. The audit does not expose every evidence and ranking completeness count enforced by the decision.

1
WITH route_checks AS (
2
SELECT
3
1::UINTEGER AS scope_order,
4
'route'::VARCHAR AS scope,
5
r.route,
6
u.check_ordinality::UBIGINT AS check_ordinality,
7
u.entry.key::VARCHAR AS check_name,
8
u.entry.value::BOOLEAN AS passed,
9
COALESCE(u.entry.value::VARCHAR, 'NULL') AS observed,
10
'true'::VARCHAR AS expected,
11
CASE
12
WHEN u.entry.value IS NOT TRUE
13
THEN COALESCE(
14
r.evidence_rejection_reason,
15
r.rejection_reason,
16
'INELIGIBLE_UNCLASSIFIED'
17
)
18
END AS rejection_reason
19
FROM route_rankings r
20
CROSS JOIN UNNEST(map_entries(r.checks))
21
WITH ORDINALITY AS u(entry, check_ordinality)
22
WHERE r.scenario = '1x'
23
),
24
global_checks AS (
25
SELECT
26
2::UINTEGER AS scope_order,
27
'global'::VARCHAR AS scope,
28
NULL::VARCHAR AS route,
29
g.check_ordinality::UBIGINT AS check_ordinality,
30
g.check_name,
31
g.passed,
32
g.observed,
33
g.expected,
34
CASE
35
WHEN g.passed IS NOT TRUE THEN g.rejection_reason
36
END AS rejection_reason
37
FROM ticket t
38
CROSS JOIN valuation v
39
CROSS JOIN code_manifest_status m
40
CROSS JOIN decision d
41
LEFT JOIN selected_route s ON true
42
CROSS JOIN LATERAL (
43
VALUES
44
(
45
1,
46
'market_context'::VARCHAR,
47
(
48
t.chain_id IS NOT DISTINCT FROM 1
49
AND t.sender_status IS NOT DISTINCT FROM 'SUPPORTED_EOA'
50
AND v.status IS NOT DISTINCT FROM 'VALUATION_OK'
51
-- 103 more lines load when JavaScript runs

8. Inspect the selected call path #

After PASS, inspect the selected execution as a normalized call tree. Each row shows which code ran, its call context, and its position in the frame hierarchy.

UNEXPECTED_TARGET identifies code outside the declared route allowlist. The root row includes the retained execution dossier for further review.

1
SELECT
2
CASE
3
WHEN c.frame_index = 0 THEN execution_evidence(s.execution_id)
4
END AS execution_evidence,
5
COALESCE(t.name, 'UNEXPECTED_TARGET') AS target,
6
c.call_kind AS kind,
7
c.value,
8
c.frame_status,
9
c.context_address,
10
c.code_address,
11
c.frame_index,
12
c.parent_frame_index
13
FROM decision d
14
JOIN selected_route s ON d.outcome = 'PASS'
15
JOIN evm.execution_frames c
16
ON c.execution_id = s.execution_id
17
AND c.attempt_kind = 'program_step'
18
LEFT JOIN expected_execution_targets t
19
ON t.candidate_key = s.route
20
AND t.address = c.code_address
21
ORDER BY c.frame_index;

Use the decision record #

The output is not only a selected route. It is a queryable decision record that keeps the pinned state, candidate set, exact calldata, execution evidence, policy checks, and rejection reasons together.

Use that record to change the candidate relation or scoring rule, rerun the analysis, and challenge the result without rebuilding a private trace schema. Gas-adjusted rankings support route review, while code identity, call context, balance changes, and effects support execution review.

The result remains bounded to the declared candidates, inputs, pinned block, and sampled gas price. execution_result_root commits only to the simulated execution, not the quote set or ranking logic. The notebook does not authorize or send a transaction.