|
| 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 %} |
0 commit comments