Output
sf.substreams.sink.database.v1.DatabaseChanges
Inputs

Package ref
uniswap-v4-base@v0.1.4Run package
CLI
Run db_out from the command line.
substreams run uniswap-v4-base@v0.1.4 db_out -e basesubstreams auth or directly on thegraph.market (see docs).README
Uniswap V4 on Base as a Substreams package, converted from uniswap-v4-base-3
(Qmbsc6XQWbiv4DfLVfaNciScqYLyDWUYjWzrFBbzzmRsMB) — the busiest subgraph on The Graph.
Built with StreamingFast's substreams-convert.
Every claim below is verified against at least one of: the chain (eth_call / eth_getLogs /
extsload), the deployed subgraph, or a live substreams run. Anything unverified is in
Known gaps, not here.
The subgraph runs pool.feeTier = event.params.fee on every swap, so a pool's configured fee is
overwritten by whatever the last swap charged. On pool 0x0a1e0f12… (ETH/$SXR):
| source | fee |
|---|---|
Initialize event at creation, block 49150754 | 3000 |
live extsload slot0 lpFee | 3000 |
subgraph pool.feeTier | 3499 |
That pool has hooks = 0x000…000, so dynamic fees are impossible and the fee is immutable in its
PoolKey. Wrong across 3,060 transactions. A second pool (0x36d7043e…, VVV/cbBTC) shows the same
3499-vs-3000 discrepancy, so it is systematic.
This package keeps pool.fee_tier (configured, from Initialize) and swap.fee (effective,
per-swap, hook-overridable) as separate columns.
Hook permissions decoded from the hook address. V4 mines addresses so a hook's capability set
lives in its low 14 bits — 14 booleans per pool, no RPC, correct for a hook nobody has seen.
Verified against live Base hooks (0x9ea93273… → flags 4160 → after_initialize + after_swap).
The subgraph stores hooks as an opaque string plus one hardcoded ArrakisHook entity.
Real gasUsed. Every subgraph swap row carries gasUsed: "0" — it hardcodes it, with its own
TODO in the source.
ERC-6909 claim tokens. V4's flash-accounting rail. 292 rows in a 400-block sample; the subgraph does not index them at all.
salt retained, so two salted positions by one sender on one tick range stay distinguishable.
Self-describing rows. A V4 Swap log carries only a poolId. store_pools records each pool
at creation and map_enriched denormalises token0/token1/fee_tier/tick_spacing/hook back
onto every swap and liquidity event, so a row stands alone.
Token metadata — symbol, name, decimals via batched eth_call, cached once per token.
Cross-checked against a direct eth_call: token 0xe7fd1ba7… reports siddesh, exact match.
Lifetime totals and per-block deltas, both. pool_stats/hook_stats are per-block deltas that
SUM over a range; pool_totals/hook_totals are lifetime figures from add-policy stores. The
accumulation lives in the store, not in SQL, because a substreams store is deterministic and
re-derived — a block replayed by a parallel backfill worker does not double-add, whereas
UPDATE ... = col + n would.
No graft. The live subgraph grafts at block 26990278 and never indexed its own early history.
substreams run db_out, 400 Base blocks from 35000000:
| table | rows |
|---|---|
| modify_liquidity | 19,007 |
| pool | 2,970 |
| swap | 2,745 |
| pool_stats / pool_totals | 1,655 / 1,655 |
| claim_token_event | 292 |
| hook_stats / hook_totals | 184 / 184 |
| position / position_event | 34 / 34 |
cargo test --lib — 92 passing, including price maths pinned against chain extsload and
the deployed subgraph on the asymmetric-decimals case (VVV/cbBTC, 18 vs 8): computed
token0Price 4676.0682880466 against the subgraph's 4676.06828804666294…
Published: https://substreams.dev/packages/uniswap-v4-base
# Use the full URL. The short reference the registry advertises
# (uniswap-v4-base@v0.1.1) does not resolve in CLI 1.18.4 — that affects every
# package in the registry, not just this one.
substreams gui https://spkg.io/v1/packages/uniswap-v4-base/v0.1.1
cargo build --target wasm32-unknown-unknown --release
substreams pack substreams.yaml
substreams run ./uniswap-v4-base-v0.1.0.spkg map_enriched \
-e base-mainnet.streamingfast.io:443 -s 25350988 -t +9000 -o jsonl
Postgres sink (chosen over ClickHouse because pool is mutable current state and ClickHouse is
insert-only):
substreams-sink-sql setup "$DSN" ./uniswap-v4-base-v0.1.0.spkg
substreams-sink-sql run "$DSN" ./uniswap-v4-base-v0.1.0.spkg
Note: v4 delta operations require a recent substreams-sink-sql; an older binary silently ignores
them.
cursors, not the exit codesubstreams-sink-sql 4.13.1 flushes on a block interval (default 1000) and drops the final
partial batch on a bounded run, then exits 0 with substreams ended correctly. Measured on a
10,000-block run: it reported reaching #34999999 while last_block_written was #34999001 —
998 blocks, 6,372 swaps and 50,315 modify_liquidity rows silently missing, exit code 0.
Re-running the identical command does not heal it: the stored cursor wins over the requested start, the sink streams the missing blocks, flushes nothing, and terminates gracefully with byte-identical row counts.
Either run unbounded, pad the stop block to initialBlock + k x flush_interval, or force a flush
every block:
substreams-sink-sql run "$DSN" ./pkg.spkg <start>:<stop> \
-e base-mainnet.streamingfast.io:443 \
--batch-block-flush-interval 1 --batch-row-flush-interval 0
And verify completion from the database, which is the only honest signal:
SELECT block_num FROM cursors; -- must equal your stop block
pool_totals / hook_totals accumulate in an add-policy store that starts at zero at the
module's initialBlock. They are true lifetime figures only when run from the shipped
initialBlock: 25350988. Lower it to test at a recent block and they silently become "since the
window started", with nothing on the row to say so. Enriched swap rows at least carry the
token0 = '' sentinel; totals have no equivalent.
store_prices maintains the native price off
the hardcoded WETH/USDC anchor and map_totals attaches amount0_usd / amount1_usd /
amount_usd / native_price_usd / priced. The maths is pinned by tests against chain
extsload and the subgraph (anchor computes 2445.38 USD/ETH against the subgraph's 2436.70,
0.36% apart — ETH moving between reads). What is NOT demonstrated is a populated USD column over
a real range: that needs the price store built from block 25350988, which exceeds the
10,000-block request limit on this tier. Filter on priced, never on amount_usd > 0 — an
unanchored swap and a genuinely zero-value swap both read 0.Donate and ProtocolFeeUpdated decoders are present but unexercised. Zero rows in the
400-block sample — and confirmed by eth_getLogs that zero such events occurred on chain in that
range, so this is absence of data, not a broken decoder. Untested in production.eth_common:filtered_events would skip nothing.store_pools at block
35000000 requires building the store from 25350988 — 9.65M blocks against a 10,000-block request
limit. Verification above used a temporary manifest with initialBlock moved forward; the
shipped manifest starts at 25350988 and sees every pool.Modules
Output
sf.substreams.sink.database.v1.DatabaseChanges
Inputs
Output
uniswap.v4.v1.EventsOutput
uniswap.v4.v1.EventsOutput
uniswap.v4.v1.EventsStore value
bigint
Update policy
add
Inputs
Store value
bigint
Update policy
add
Inputs
Store value
proto:uniswap.v4.v1.PoolUpdate policy
set
Inputs
Store value
string
Update policy
set
Inputs
Store value
string
Update policy
set
Inputs