feat(dex): deterministic 0x Settler aggregator volume — supersede #9795 (CUR2-2844)#9800
Conversation
41b168e to
68934b0
Compare
45615c7 to
ed06f94
Compare
68934b0 to
4d9a688
Compare
ed06f94 to
6a90020
Compare
4d9a688 to
51d86d1
Compare
6a90020 to
f7897e7
Compare
51d86d1 to
d8d0953
Compare
f7897e7 to
0ee8dbc
Compare
d8d0953 to
022418e
Compare
0ee8dbc to
491b4a5
Compare
022418e to
80c6ccf
Compare
491b4a5 to
a00011e
Compare
4a1e1e0 to
9f732a5
Compare
PR SummaryHigh Risk Overview
All 17 Reviewed by Cursor Bugbot for commit 9f732a5. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Missing sentinel zid exclusion
- Added the missing sentinel zid filter to the settler aggregator input so those staged rows cannot emit aggregator trades.
- ✅ Fixed: CoW settler log scoping removed
- Restored CoW-specific transfer scoping by carrying cow_rn through the aggregator and limiting CoW leg matching to pre-settlement-window transfers involving the settler or GPv2 settlement contract.
Or push these changes by commenting:
@cursor push 795034933e
Preview (795034933e)
diff --git a/dbt_subprojects/dex/macros/models/_project/zeroex/zeroex_settler_agg.sql b/dbt_subprojects/dex/macros/models/_project/zeroex/zeroex_settler_agg.sql
--- a/dbt_subprojects/dex/macros/models/_project/zeroex/zeroex_settler_agg.sql
+++ b/dbt_subprojects/dex/macros/models/_project/zeroex/zeroex_settler_agg.sql
@@ -5,6 +5,9 @@
{%- set weth = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' -%}
{%- set native_tokens = '(0x0000000000000000000000000000000000000000, 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee)' -%}
{%- set erc20_transfer_topic = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' -%}
+{%- set cow_trade_topic = '0xed99827efb37016f2275f98c4bcf71c7551c75d59e9b450f79fa32e60be672c2' -%}
+{%- set cow_protocol_settlement = '0x9008d19f58aabd9ed0d60971565aa8510560ab41' -%}
+{%- set sentinel_zid = '0xa00000000000000000000000' -%}
-- Deterministic 0x Settler aggregator decode (replaces the heuristic leg-matching + the PR #9795 band-aid).
-- Per settler call, emits ONE 0x-API aggregator row = the user's net swap:
@@ -21,7 +24,7 @@
WITH settler AS (
SELECT
- tx_hash, block_time, block_number, method_id, settler_address, zid, tag, rn,
+ tx_hash, block_time, block_number, method_id, settler_address, zid, tag, rn, cow_rn,
buy_token, min_amount_out, settler_msgsender
FROM {{ ref('zeroex_v2_' ~ blockchain ~ '_settler_txs') }}
{% if is_incremental() %}
@@ -29,6 +32,7 @@
{% else %}
WHERE block_time >= DATE '{{ start_date }}'
{% endif %}
+ AND zid != {{ sentinel_zid }}
),
-- Distinct partition key of the settler txs, so the transactions/logs scans prune by partition
@@ -54,7 +58,7 @@
calls AS (
SELECT
- s.tx_hash, s.block_time, s.block_number, s.settler_address, s.zid, s.tag, s.rn, s.min_amount_out,
+ 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,
t.tx_from, t.tx_to,
-- receiver (the user): execute -> tx-level sender; executeMetaTxn -> msgSender (relayer pays gas).
CASE WHEN s.method_id = 0xfd3ad6d4 THEN s.settler_msgsender ELSE t.tx_from END AS receiver,
@@ -64,26 +68,30 @@
LEFT JOIN txs t ON t.tx_hash = s.tx_hash
),
-transfers AS (
+tx_logs AS (
SELECT
logs.block_number,
logs.tx_hash,
+ logs.index AS log_index,
logs.contract_address AS token,
- varbinary_substring(logs.topic1, 13, 20) AS transfer_from,
- varbinary_substring(logs.topic2, 13, 20) AS transfer_to,
- -- CASE-guard the conversion (Trino only evaluates the THEN branch when the WHEN holds): a non-standard
- -- Transfer-topic log with >32-byte data would otherwise overflow bytearray_to_uint256. The WHERE filter
- -- below is not sufficient on its own — Trino may evaluate this projection before applying it.
- CASE WHEN varbinary_length(logs.data) = 32 THEN bytearray_to_uint256(logs.data) END AS amount
+ logs.topic0,
+ logs.topic1,
+ logs.topic2,
+ logs.data,
+ max(CASE
+ WHEN logs.topic0 = {{ cow_trade_topic }}
+ AND varbinary_substring(logs.topic1, 13, 20) != 0xDef1C0ded9bec7F1a1670819833240f027b25EfF
+ THEN logs.index
+ END) OVER (PARTITION BY logs.block_number, logs.tx_hash) AS cow_max_index
FROM {{ source(blockchain, 'logs') }} AS logs
JOIN settler_tx_keys k
ON k.block_time = logs.block_time
AND k.block_number = logs.block_number
AND k.tx_hash = logs.tx_hash
- WHERE logs.topic0 = {{ erc20_transfer_topic }}
+ WHERE logs.topic0 IN ({{ erc20_transfer_topic }}, {{ cow_trade_topic }})
-- standard ERC20 value transfers only (uint256 amount is exactly 32 bytes): drops NFT/ERC721 (0-byte
- -- data) and non-standard >32-byte Transfer-topic logs. Row-reducer; the overflow guard is the CASE above.
- AND varbinary_length(logs.data) = 32
+ -- data) and non-standard >32-byte Transfer-topic logs. Row-reducer; the overflow guard is the CASE below.
+ AND (logs.topic0 != {{ erc20_transfer_topic }} OR varbinary_length(logs.data) = 32)
{% if is_incremental() %}
AND {{ incremental_predicate('logs.block_time') }}
{% else %}
@@ -91,6 +99,23 @@
{% endif %}
),
+transfers AS (
+ SELECT
+ logs.block_number,
+ logs.tx_hash,
+ logs.log_index,
+ logs.token,
+ varbinary_substring(logs.topic1, 13, 20) AS transfer_from,
+ varbinary_substring(logs.topic2, 13, 20) AS transfer_to,
+ -- CASE-guard the conversion (Trino only evaluates the THEN branch when the WHEN holds): a non-standard
+ -- Transfer-topic log with >32-byte data would otherwise overflow bytearray_to_uint256. The WHERE filter
+ -- below is not sufficient on its own — Trino may evaluate this projection before applying it.
+ CASE WHEN varbinary_length(logs.data) = 32 THEN bytearray_to_uint256(logs.data) END AS amount,
+ logs.cow_max_index
+ FROM tx_logs AS logs
+ WHERE logs.topic0 = {{ erc20_transfer_topic }}
+),
+
-- Single pass over the transfers: buy leg = unique Transfer(buyToken, to=receiver); sell leg (best-effort) =
-- unique Transfer out of the user of a token other than buyToken.
legs AS (
@@ -106,6 +131,16 @@
AND t.block_number = c.block_number
AND t.amount > UINT256 '0'
AND (
+ c.cow_rn IS NULL
+ OR (
+ t.log_index < t.cow_max_index
+ AND (
+ t.transfer_from IN (c.settler_address, {{ cow_protocol_settlement }})
+ OR t.transfer_to IN (c.settler_address, {{ cow_protocol_settlement }})
+ )
+ )
+ )
+ AND (
(t.token = c.buy_token AND t.transfer_to = c.receiver)
OR (t.transfer_from = c.receiver AND t.token <> c.buy_token)
)You can send follow-ups to the cloud agent here.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9f732a5. Configure here.
9f732a5 to
fa57ea2
Compare
|
Regression found one blocker in the CI-built output. For
It has:
The corresponding CI staging row has This is isolated to one Base row over Uniqueness passed across the non-empty CI |
|
Also, one deployment-path note before merge: the downstream refresh plan should include the I checked prod via Dune: all 17
They do not yet have the columns this PR’s
So a historical full refresh of the trades models would read incomplete/null staging data unless the corresponding |
tomfutago
left a comment
There was a problem hiding this comment.
@thevaizman plz address regression blocker
fa57ea2 to
7749f66
Compare
|
@tomfutago thanks for the thorough regression — both items addressed in Blocker ( Deploy-path note (staging tables): great catch. Updated the PR description's deploy note to make the order explicit — refresh/schema-sync all 17 Re-requesting your review 🙏 |
7749f66 to
78e059b
Compare
|
@tomfutago follow-up on the blocker — I investigated all 22 of these end-to-end ( So I moved the fix to the root: the settler-txs staging now excludes reverted traces ( Pushed in |
… decoder, 16 chains) Promotes 0x Settler plain-RFQ (action selector 0xd92aadfb) maker fills into dex.trades as a new PMM venue (project '0x API', version 'settler') across 16 chains. They were previously absent from dex.trades (which only carried the legacy ExchangeProxy decodes). Method, ported from Dune's echo indexer (zero_ex_settler.rs): - Token identities (maker, makerAsset, takerToken) come from the signed RFQ action's calldata static head; amounts come from the real ERC20 Transfer logs pivoted on the maker, with an exactly-one-per-leg validity gate (drops ambiguous / native-ETH legs). - Identity-grouped maker pivot so distinct fills sharing a byte offset across multiple settler traces in one tx are not collapsed (the naive (tx_hash,p) grouping silently dropped/aliased such fills via arbitrary()). Files: - new macro zeroex_settler_rfq + per-chain zeroex_settler_<chain>_base_trades (16 chains), registered in dex_<chain>_base_trades; dex_info + ethereum seed test added. - settler-txs staging extended to emit tx_from + rfq_input (RFQ-bearing rows only). Verified on-chain (Ethereum + sampled chains): decoder reproduces real fills exactly; 0 overlap with the legacy native 0x venue (no double counting); AMM-routed settler trades correctly excluded (their underlying pool venues already represent them). mode excluded (no dex pipeline). dex_aggregator path unchanged in this PR. CUR2-2843 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…imeout CI slim-builds full-refresh dex_<chain>_base_trades (the duplicates_rank window over a chain's entire history), exceeding the per-node memory limit on large chains (bnb: 331GB TopN vs 348GB ceiling) and the CI job timeout. Bound the union to the last 7 days in CI only (target=ci); prod (dunesql) is unchanged and keeps the incremental_predicate. Verified no CI test depends on full-history dex_base_trades: aggregate models carry no history-dependent tests (only dex_story's check_dex_info_relationship, which passes on a subset), seed tests query venue models (not the aggregate), and dex_<chain>_trades is out of CI scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 0xd92aadfb selector can recur inside an RFQ action's ABI body, so the all-occurrences scan decodes the SAME action twice (identical maker/asset/token) at two byte offsets, both matching the one maker-leg transfer -> duplicate (tx_hash, evt_index). Keep one row per (tx_hash, maker_evt_index) via row_number; genuine distinct fills always have distinct maker-leg logs, so only the spurious re-decode is dropped (verified: the lone ethereum 14d collision was an identical-tuple recurrence at offsets 1093/1125 of tx 0x49da06f1...). Uses a subquery rather than QUALIFY (no precedent for QUALIFY in this codebase). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
274bda7 to
fd7d76a
Compare
78e059b to
b85663d
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>


What
Replaces the heuristic maker/taker leg-matching (and PR #9795's 5×-divergence band-aid) for 0x Settler trades in
dex_aggregator.tradeswith a deterministic decode. Per settler call → one0x APIaggregator row = the user's net swap.Method (verified across 3 on-chain R&D rounds)
AllowedSlippage.buyToken(calldata, deterministic).Transfer(buyToken, to=receiver)when resolvable, else theminAmountOutfloor (verified tight — median ~1% under actual), guarded against "no minimum" sentinels (minAmountOut ≥ 2^128→ null, never the floor — see the0xc01c2829…blocker fix below).execute) /msgSender(executeMetaTxn) — verified to be the true buyToken recipient (reconciled on0xf61374…: the calldatarecipientis an intermediate hop;trace.fromis the AllowanceHolder). Anchoring on a single calldata-named token to the verified user means it can't mis-bind to internal routing hops the way the heuristic did.cow_rnset, ~0.5% of settler calls): the tx-level receiver is the CoW solver/settlement, not the user, so the receiver-pivot is unreliable amid the batch → use the deterministicminAmountOutfloor + null sell leg (buyToken stays the calldata value). The deterministic analogue ofzeroex_v2'scow_log_rangewindow scoping.Transferout of the user, token ≠ buyToken).add_amount_usd) → an unpriced buyToken still values off the sell leg.Why it supersedes #9795
#9795 is a coarse 5×-threshold proxy that only patches
volume_usdand leaves the wrong token columns. This computes the user's net swap deterministically. Example0xf61374…: current $0.75 → deterministic ~$497.Files
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>_tradesrewired to it. Restores thezid != 0xa00…sentinel exclusion, adds CoW floor-fallback, and preserves berachain/ink start dates (2025-02-15).Validation
dex_info).zid, CoW scoping, berachain/ink start dates, and theminAmountOut = UINT256_MAXblocker (Base0xc01c2829…, was ~1e71 USD) — now guarded.zeroex_v2_<chain>_settler_txsstaging tables — they exist in prod but lack this PR's newbuy_token/min_amount_out/settler_msgsendercolumns, so a trades full-refresh would read null staging until they're synced (thanks @tomfutago for catching this dependency).zeroex_v2_<chain>_trades.dex_aggregator_trades(merge can't delete the old mis-bound rows — the CUR2-2693 caution).Stacked on #9798 (CUR2-2843). CUR2-2844
🤖 Generated with Claude Code