Skip to content

Commit 78e059b

Browse files
thevaizmanclaude
andcommitted
feat(dex): deterministic 0x Settler aggregator volume — supersede PR #9795 (CUR2-2844)
Replaces the heuristic maker/taker leg-matching (and the PR #9795 5x-divergence band-aid) for 0x Settler trades in dex_aggregator.trades with a deterministic decode. Per settler call, emits one 0x-API aggregator row = the user's net swap: - token_bought = AllowedSlippage.buyToken (from calldata, deterministic). - amount = the unique Transfer(buyToken, to=receiver) when resolvable, else the minAmountOut floor (verified tight, ~1% under actual). receiver = tx-level sender (execute) / msgSender (executeMetaTxn) — verified to be the true buyToken recipient, NOT the calldata recipient (an intermediate routing hop). Anchoring on a single calldata-named token to the verified user means it cannot mis-bind to internal routing hops the way the heuristic did. - token_sold = best-effort (unique Transfer out of the user, token != buyToken). - volume_usd priced via either leg (add_amount_usd), so an unpriced buyToken still values off the sell leg (cuts the unpriced-NULL bucket ~20% -> ~5%). New macro zeroex_settler_agg; settler-txs staging extended with the AllowedSlippage fields (buy_token, min_amount_out, settler_msgsender); all 17 settler chains' zeroex_v2_<chain>_trades rewired to it. Verified on-chain (example 0xf61374… : current $0.75 -> deterministic ~$490). NOTE: requires a coordinated full-refresh of zeroex_v2_<chain>_trades + dex_aggregator_trades (merge cannot delete the old mis-bound rows). Old-vs-new regression to be validated on CI-built tables before merge. Stacked on CUR2-2843 (Phase 1). CUR2-2844 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 274bda7 commit 78e059b

19 files changed

Lines changed: 301 additions & 783 deletions
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
{% macro zeroex_settler_agg(blockchain, start_date='2024-07-15') %}
2+
{%- if target.name == 'ci' -%}
3+
{%- set start_date = (modules.datetime.date.today() - modules.datetime.timedelta(days=14)).strftime('%Y-%m-%d') -%}
4+
{%- endif -%}
5+
{%- set weth = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' -%}
6+
{%- set native_tokens = '(0x0000000000000000000000000000000000000000, 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee)' -%}
7+
{%- set erc20_transfer_topic = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' -%}
8+
{#- 2^128. minAmountOut at/above this is a "no minimum" sentinel (callers scatter across the top of the
9+
uint256 range, incl. UINT256_MAX), not a real token amount — even a $1B trade is well under 2^128.
10+
The floor fallback must ignore these or it emits absurd volume (e.g. ~1e71 USD). -#}
11+
{%- set max_plausible_amount = '340282366920938463463374607431768211456' -%}
12+
13+
-- Deterministic 0x Settler aggregator decode (replaces the heuristic leg-matching + the PR #9795 band-aid).
14+
-- Per settler call, emits ONE 0x-API aggregator row = the user's net swap:
15+
-- token_bought = AllowedSlippage.buyToken (from calldata, deterministic).
16+
-- token_bought amount = the unique Transfer(buyToken, to=receiver) when resolvable, else the minAmountOut
17+
-- floor (verified tight, ~1% under actual). receiver = the tx-level sender (execute) / msgSender
18+
-- (executeMetaTxn) — verified to be the true buyToken recipient, NOT the calldata `recipient` (which is
19+
-- often an intermediate routing hop). Anchoring on a single calldata-named token to the verified user
20+
-- means it cannot mis-bind to internal routing hops the way the heuristic did.
21+
-- token_sold = best-effort: the unique Transfer(from=receiver, token != buyToken).
22+
-- volume_usd = priced via either leg (add_amount_usd), so an unpriced buyToken still values off the sell leg.
23+
-- NOTE: native-ETH buyToken is mapped to mainnet WETH for pricing, so it only resolves a price on ethereum;
24+
-- per-chain wrapped-native pricing for native buys is a tracked follow-up (off-mainnet native buys -> NULL volume).
25+
26+
WITH settler AS (
27+
SELECT
28+
tx_hash, block_time, block_number, method_id, settler_address, zid, tag, rn, cow_rn,
29+
buy_token, min_amount_out, settler_msgsender
30+
FROM {{ ref('zeroex_v2_' ~ blockchain ~ '_settler_txs') }}
31+
-- exclude the sentinel zid (non-trade fills), matching the zeroex_v2 pipeline this replaces
32+
WHERE zid != 0xa00000000000000000000000
33+
{% if is_incremental() %}
34+
AND {{ incremental_predicate('block_time') }}
35+
{% else %}
36+
AND block_time >= DATE '{{ start_date }}'
37+
{% endif %}
38+
),
39+
40+
-- Distinct partition key of the settler txs, so the transactions/logs scans prune by partition
41+
-- (block_time + block_number + tx_hash) rather than a tx_hash-only post-scan filter, per zeroex_v2.sql's norm.
42+
settler_tx_keys AS (
43+
SELECT DISTINCT block_time, block_number, tx_hash FROM settler
44+
),
45+
46+
-- Tx-level sender/recipient (the settler trace's own `from` is an intermediary for nested/Relay-routed calls).
47+
txs AS (
48+
SELECT t.hash AS tx_hash, t."from" AS tx_from, t."to" AS tx_to
49+
FROM {{ source(blockchain, 'transactions') }} t
50+
JOIN settler_tx_keys k
51+
ON k.block_time = t.block_time
52+
AND k.block_number = t.block_number
53+
AND k.tx_hash = t.hash
54+
{% if is_incremental() %}
55+
WHERE {{ incremental_predicate('t.block_time') }}
56+
{% else %}
57+
WHERE t.block_time >= DATE '{{ start_date }}'
58+
{% endif %}
59+
),
60+
61+
calls AS (
62+
SELECT
63+
s.tx_hash, s.block_time, s.block_number, s.settler_address, s.zid, s.tag, s.rn, s.cow_rn,
64+
-- guard the floor against "no minimum" sentinels (minAmountOut >= 2^128): null them so the
65+
-- fallback yields null volume rather than an absurd amount. real amounts are far below 2^128.
66+
CASE WHEN s.min_amount_out < UINT256 '{{ max_plausible_amount }}' THEN s.min_amount_out END AS min_amount_out,
67+
t.tx_from, t.tx_to,
68+
-- receiver (the user): execute -> tx-level sender; executeMetaTxn -> msgSender (relayer pays gas).
69+
CASE WHEN s.method_id = 0xfd3ad6d4 THEN s.settler_msgsender ELSE t.tx_from END AS receiver,
70+
-- native sentinels represented/priced as WETH (see header note)
71+
CASE WHEN s.buy_token IN {{ native_tokens }} THEN {{ weth }} ELSE s.buy_token END AS buy_token
72+
FROM settler s
73+
LEFT JOIN txs t ON t.tx_hash = s.tx_hash
74+
),
75+
76+
transfers AS (
77+
SELECT
78+
logs.block_number,
79+
logs.tx_hash,
80+
logs.contract_address AS token,
81+
varbinary_substring(logs.topic1, 13, 20) AS transfer_from,
82+
varbinary_substring(logs.topic2, 13, 20) AS transfer_to,
83+
-- CASE-guard the conversion (Trino only evaluates the THEN branch when the WHEN holds): a non-standard
84+
-- Transfer-topic log with >32-byte data would otherwise overflow bytearray_to_uint256. The WHERE filter
85+
-- below is not sufficient on its own — Trino may evaluate this projection before applying it.
86+
CASE WHEN varbinary_length(logs.data) = 32 THEN bytearray_to_uint256(logs.data) END AS amount
87+
FROM {{ source(blockchain, 'logs') }} AS logs
88+
JOIN settler_tx_keys k
89+
ON k.block_time = logs.block_time
90+
AND k.block_number = logs.block_number
91+
AND k.tx_hash = logs.tx_hash
92+
WHERE logs.topic0 = {{ erc20_transfer_topic }}
93+
-- standard ERC20 value transfers only (uint256 amount is exactly 32 bytes): drops NFT/ERC721 (0-byte
94+
-- data) and non-standard >32-byte Transfer-topic logs. Row-reducer; the overflow guard is the CASE above.
95+
AND varbinary_length(logs.data) = 32
96+
{% if is_incremental() %}
97+
AND {{ incremental_predicate('logs.block_time') }}
98+
{% else %}
99+
AND logs.block_time >= DATE '{{ start_date }}'
100+
{% endif %}
101+
),
102+
103+
-- Single pass over the transfers: buy leg = unique Transfer(buyToken, to=receiver); sell leg (best-effort) =
104+
-- unique Transfer out of the user of a token other than buyToken.
105+
legs AS (
106+
SELECT c.tx_hash, c.rn,
107+
count(*) FILTER (WHERE t.token = c.buy_token AND t.transfer_to = c.receiver) AS buy_n,
108+
arbitrary(t.amount) FILTER (WHERE t.token = c.buy_token AND t.transfer_to = c.receiver) AS buy_amount,
109+
count(*) FILTER (WHERE t.transfer_from = c.receiver AND t.token <> c.buy_token) AS sell_n,
110+
arbitrary(t.token) FILTER (WHERE t.transfer_from = c.receiver AND t.token <> c.buy_token) AS sell_token,
111+
arbitrary(t.amount) FILTER (WHERE t.transfer_from = c.receiver AND t.token <> c.buy_token) AS sell_amount
112+
FROM calls c
113+
JOIN transfers t
114+
ON t.tx_hash = c.tx_hash
115+
AND t.block_number = c.block_number
116+
AND t.amount > UINT256 '0'
117+
AND (
118+
(t.token = c.buy_token AND t.transfer_to = c.receiver)
119+
OR (t.transfer_from = c.receiver AND t.token <> c.buy_token)
120+
)
121+
GROUP BY c.tx_hash, c.rn
122+
),
123+
124+
token_metadata AS (
125+
SELECT contract_address, symbol, decimals
126+
FROM {{ source('tokens', 'erc20') }}
127+
WHERE blockchain = '{{ blockchain }}'
128+
),
129+
130+
trades AS (
131+
SELECT
132+
c.block_time, c.block_number, c.tx_hash, c.tx_from, c.tx_to, c.zid, c.tag,
133+
c.rn AS evt_index, c.settler_address AS contract_address, c.receiver AS taker,
134+
c.buy_token AS token_bought_address,
135+
-- CoW-batched settler fills (cow_rn set): tx-level receiver is the CoW solver/settlement, not the user,
136+
-- so the receiver-pivot transfer match is unreliable amid the whole batch's transfers. Fall back to the
137+
-- deterministic minAmountOut floor for the bought leg and leave the sell leg null rather than risk
138+
-- mis-assigning another order's transfer. buyToken itself is still the deterministic calldata value.
139+
CASE WHEN c.cow_rn IS NOT NULL THEN c.min_amount_out
140+
WHEN l.buy_n = 1 THEN l.buy_amount
141+
ELSE c.min_amount_out END AS token_bought_amount_raw,
142+
CASE WHEN c.cow_rn IS NULL AND l.sell_n = 1 THEN l.sell_token END AS token_sold_address,
143+
CASE WHEN c.cow_rn IS NULL AND l.sell_n = 1 THEN l.sell_amount END AS token_sold_amount_raw
144+
FROM calls c
145+
LEFT JOIN legs l ON l.tx_hash = c.tx_hash AND l.rn = c.rn
146+
),
147+
148+
results AS (
149+
SELECT
150+
'{{ blockchain }}' AS blockchain,
151+
trades.block_time, trades.block_number, trades.tx_hash, trades.tx_from, trades.tx_to,
152+
trades.zid, trades.tag, trades.evt_index, trades.contract_address, trades.taker,
153+
CAST(NULL AS varbinary) AS maker,
154+
trades.token_bought_address,
155+
bt.symbol AS bought_symbol,
156+
trades.token_bought_amount_raw,
157+
trades.token_bought_amount_raw / POW(10, bt.decimals) AS token_bought_amount,
158+
trades.token_sold_address,
159+
st.symbol AS sold_symbol,
160+
trades.token_sold_amount_raw,
161+
trades.token_sold_amount_raw / POW(10, st.decimals) AS token_sold_amount
162+
FROM trades
163+
LEFT JOIN token_metadata bt ON bt.contract_address = trades.token_bought_address
164+
LEFT JOIN token_metadata st ON st.contract_address = trades.token_sold_address
165+
),
166+
167+
results_usd AS (
168+
{{ add_amount_usd(trades_cte = 'results') }}
169+
)
170+
171+
SELECT
172+
'{{ blockchain }}' AS blockchain,
173+
'0x-API' AS project,
174+
'settler' AS version,
175+
cast(DATE_TRUNC('day', block_time) as date) AS block_date,
176+
cast(DATE_TRUNC('month', block_time) as date) AS block_month,
177+
block_time,
178+
block_number,
179+
sold_symbol AS taker_symbol,
180+
bought_symbol AS maker_symbol,
181+
CASE WHEN LOWER(sold_symbol) > LOWER(bought_symbol) THEN CONCAT(bought_symbol, '-', sold_symbol) ELSE CONCAT(sold_symbol, '-', bought_symbol) END AS token_pair,
182+
token_sold_amount AS taker_token_amount,
183+
token_bought_amount AS maker_token_amount,
184+
token_sold_amount_raw AS taker_token_amount_raw,
185+
token_bought_amount_raw AS maker_token_amount_raw,
186+
amount_usd AS volume_usd,
187+
token_sold_address AS taker_token,
188+
token_bought_address AS maker_token,
189+
taker,
190+
maker,
191+
tag,
192+
zid,
193+
tx_hash,
194+
tx_from,
195+
tx_to,
196+
evt_index,
197+
(ARRAY[-1]) AS trace_address,
198+
'settler' AS type,
199+
TRUE AS swap_flag,
200+
contract_address
201+
FROM results_usd
202+
{% endmacro %}

dbt_subprojects/dex/macros/models/_project/zeroex/zeroex_settler_txs_cte.sql

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ filtered_traces AS (
3232
WHERE
3333
-- Filter for specific method signatures used by 0x Protocol
3434
(varbinary_position(input,0x1fff991f) <> 0 OR varbinary_position(input,0xfd3ad6d4) <> 0)
35+
-- Exclude reverted settler calls (verified: failed ERC-4337 UserOp-wrapped settler calls that execute
36+
-- no swap and emit no transfers — genuine non-trades). Without this, a reverted call carrying a sentinel
37+
-- minAmountOut reaches the aggregator's floor fallback and emits absurd volume; the RFQ path already
38+
-- drops them via its maker-pivot transfer gate, so this only removes non-trades.
39+
AND success
3540
-- Apply time-based filtering for incremental loads
3641
{% if is_incremental() %}
3742
AND {{ incremental_predicate('block_time') }}
@@ -110,7 +115,13 @@ settler_txs AS (
110115
END AS taker,
111116
-- Keep raw calldata only for RFQ-bearing settler calls (plain RFQ action 0xd92aadfb);
112117
-- consumed by the zeroex_settler_rfq macro. NULL otherwise to avoid bloating the staging table.
113-
CASE WHEN varbinary_position(input, 0xd92aadfb) <> 0 THEN input END AS rfq_input
118+
CASE WHEN varbinary_position(input, 0xd92aadfb) <> 0 THEN input END AS rfq_input,
119+
-- AllowedSlippage fields (present in every execute/executeMetaTxn call, fixed offsets) — used by
120+
-- the deterministic aggregator decode (zeroex_settler_agg): buyToken, minAmountOut, and the
121+
-- executeMetaTxn msgSender (the order signer / fund owner; tx-level from is the relayer there).
122+
varbinary_substring(input, 49, 20) AS buy_token,
123+
bytearray_to_uint256(varbinary_substring(input, 69, 32)) AS min_amount_out,
124+
CASE WHEN method_id = 0xfd3ad6d4 THEN varbinary_substring(input, 177, 20) END AS settler_msgsender
114125
FROM
115126
settler_trace_data
116127
WHERE

dbt_subprojects/dex/models/_projects/zeroex/arbitrum/zeroex_v2_arbitrum_trades.sql

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,49 +10,8 @@
1010
incremental_predicates = [incremental_predicate('DBT_INTERNAL_DEST.block_time')]
1111
)}}
1212

13-
{% set zeroex_settler_start_date = '2024-07-15' %}
14-
{% set blockchain = 'arbitrum' %}
15-
16-
WITH zeroex_tx AS (
17-
-- Read the pre-materialized settler transactions instead of inlining the
18-
-- zeroex_settler_txs_cte macro, which Trino re-expanded into ~14 arbitrum.traces scans.
19-
select
20-
tx_hash,
21-
block_time,
22-
block_number,
23-
method_id,
24-
contract_address,
25-
settler_address,
26-
zid,
27-
tag,
28-
rn,
29-
cow_rn,
30-
taker
31-
from {{ ref('zeroex_v2_arbitrum_settler_txs') }}
32-
{% if is_incremental() %}
33-
where {{ incremental_predicate('block_time') }}
34-
{% endif %}
35-
),
36-
zeroex_v2_trades AS (
37-
{{
38-
zeroex_v2_trades(
39-
blockchain = blockchain,
40-
start_date = zeroex_settler_start_date
41-
42-
)
43-
}}
44-
),
45-
46-
trade_details as (
47-
{{
48-
zeroex_v2_trades_detail(
49-
blockchain = blockchain,
50-
start_date = zeroex_settler_start_date
51-
52-
)
53-
}}
54-
55-
)
56-
select
57-
*
58-
from trade_details
13+
{{
14+
zeroex_settler_agg(
15+
blockchain = 'arbitrum'
16+
)
17+
}}

dbt_subprojects/dex/models/_projects/zeroex/avalanche_c/zeroex_v2_avalanche_c_trades.sql

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,49 +10,8 @@
1010
incremental_predicates = [incremental_predicate('DBT_INTERNAL_DEST.block_time')]
1111
)}}
1212

13-
{% set zeroex_settler_start_date = '2024-07-15' %}
14-
{% set blockchain = 'avalanche_c' %}
15-
16-
WITH zeroex_tx AS (
17-
-- Read the pre-materialized settler transactions instead of inlining the
18-
-- zeroex_settler_txs_cte macro, which Trino re-expanded into ~14 avalanche_c.traces scans.
19-
select
20-
tx_hash,
21-
block_time,
22-
block_number,
23-
method_id,
24-
contract_address,
25-
settler_address,
26-
zid,
27-
tag,
28-
rn,
29-
cow_rn,
30-
taker
31-
from {{ ref('zeroex_v2_avalanche_c_settler_txs') }}
32-
{% if is_incremental() %}
33-
where {{ incremental_predicate('block_time') }}
34-
{% endif %}
35-
),
36-
zeroex_v2_trades AS (
37-
{{
38-
zeroex_v2_trades(
39-
blockchain = blockchain,
40-
start_date = zeroex_settler_start_date
41-
42-
)
43-
}}
44-
),
45-
46-
trade_details as (
47-
{{
48-
zeroex_v2_trades_detail(
49-
blockchain = blockchain,
50-
start_date = zeroex_settler_start_date
51-
52-
)
53-
}}
54-
55-
)
56-
select
57-
*
58-
from trade_details
13+
{{
14+
zeroex_settler_agg(
15+
blockchain = 'avalanche_c'
16+
)
17+
}}

dbt_subprojects/dex/models/_projects/zeroex/base/zeroex_v2_base_trades.sql

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,49 +10,8 @@
1010
incremental_predicates = [incremental_predicate('DBT_INTERNAL_DEST.block_time')]
1111
)}}
1212

13-
{% set zeroex_settler_start_date = '2024-07-15' %}
14-
{% set blockchain = 'base' %}
15-
16-
WITH zeroex_tx AS (
17-
-- Read the pre-materialized settler transactions instead of inlining the
18-
-- zeroex_settler_txs_cte macro, which Trino re-expanded into ~14 base.traces scans.
19-
select
20-
tx_hash,
21-
block_time,
22-
block_number,
23-
method_id,
24-
contract_address,
25-
settler_address,
26-
zid,
27-
tag,
28-
rn,
29-
cow_rn,
30-
taker
31-
from {{ ref('zeroex_v2_base_settler_txs') }}
32-
{% if is_incremental() %}
33-
where {{ incremental_predicate('block_time') }}
34-
{% endif %}
35-
),
36-
zeroex_v2_trades AS (
37-
{{
38-
zeroex_v2_trades(
39-
blockchain = blockchain,
40-
start_date = zeroex_settler_start_date
41-
42-
)
43-
}}
44-
),
45-
46-
trade_details as (
47-
{{
48-
zeroex_v2_trades_detail(
49-
blockchain = blockchain,
50-
start_date = zeroex_settler_start_date
51-
52-
)
53-
}}
54-
55-
)
56-
select
57-
*
58-
from trade_details
13+
{{
14+
zeroex_settler_agg(
15+
blockchain = 'base'
16+
)
17+
}}

0 commit comments

Comments
 (0)