Skip to content

Commit fa57ea2

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 fa57ea2

19 files changed

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

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,13 @@ settler_txs AS (
110110
END AS taker,
111111
-- Keep raw calldata only for RFQ-bearing settler calls (plain RFQ action 0xd92aadfb);
112112
-- 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
113+
CASE WHEN varbinary_position(input, 0xd92aadfb) <> 0 THEN input END AS rfq_input,
114+
-- AllowedSlippage fields (present in every execute/executeMetaTxn call, fixed offsets) — used by
115+
-- the deterministic aggregator decode (zeroex_settler_agg): buyToken, minAmountOut, and the
116+
-- executeMetaTxn msgSender (the order signer / fund owner; tx-level from is the relayer there).
117+
varbinary_substring(input, 49, 20) AS buy_token,
118+
bytearray_to_uint256(varbinary_substring(input, 69, 32)) AS min_amount_out,
119+
CASE WHEN method_id = 0xfd3ad6d4 THEN varbinary_substring(input, 177, 20) END AS settler_msgsender
114120
FROM
115121
settler_trace_data
116122
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)