All packages

aerodrome_substreams

PaulieB14
v1.0.0/0 downloads

Package ref

aerodrome-substreams@v1.0.0

Run package

CLI

Run db_out from the command line.

substreams run aerodrome-substreams@v1.0.0 db_out -e base
Authenticate by running substreams auth or directly on thegraph.market (see docs).

README

Aerodrome Finance Substreams

Substreams Base Aerodrome SQL Sink

Substreams for Aerodrome Finance on Base, covering both Aerodrome venues and the Coinbase tokenized stocks that launched on it.

VenueEventsCovered
Aerodrome v2 — volatile / stable AMMSwap, Mint, Burn, Sync✅
Slipstream — concentrated liquiditySwap, Mint, Burn, Collect✅
Base B20 — tokenized stocks and other RWAs19 token events incl. corporate actions✅

Tokenized stocks

v1.0.0 adds an issuer-verified stock registry, per-trade stock outputs with multiplier-adjusted share amounts and USD notional, daily per-ticker volume, a corporate-actions table and Voter gauge/vote tables. See v1.0.0: Coinbase stocks on Aerodrome.

Coinbase's tokenized stocks are B20 tokens. On Aerodrome the deep markets are Slipstream concentrated-liquidity pools, but they trade on both venues — GOOGLc/AERO and NVDAc/USDC are live v2 AMM pools — so both rails are indexed. Slipstream inherits Uniswap V3's event ABI, a different Swap signature from the v2 AMM:

VenueSwap signaturetopic0
Aerodrome v2Swap(address,address,uint256,uint256,uint256,uint256)0xb3e27736…
SlipstreamSwap(address,address,int256,int256,uint160,uint128,int24)0xc42079f9…

A v2-only decoder shares no topic with the stock pools, so it sees none of that volume. Both are decoded here.

New tickers index themselves

Pools are discovered from factory PoolCreated events rather than from a hardcoded list, and B20-ness is recovered from the token address itself. Every B20 is created by the singleton IB20Factory precompile at a deterministic address:

[ B20 prefix: 10 bytes ][ variant: 1 byte ][ bytes9(keccak256(deployer, salt)) ]
  0xb2 + nine 0x00        0x00 = Asset
                          0x01 = Stablecoin

So when a new tokenized asset lists and someone opens an Aerodrome pool for it, the pool appears in the registry on the block it is created, already flagged — no code change, no repackage, no resync. The variant byte is read straight from the address, so Asset and Stablecoin tokens are distinguished with no RPC call, and a variant introduced by a later hardfork still indexes rather than being dropped.

Matching on 11 bytes — the prefix plus a hardcoded Asset variant byte — is a tempting shortcut and a wrong one: it silently rejects every Stablecoin and every future variant. Verified against the live precompile, which reports isB20 == true for the 10-byte prefix with any variant byte and false as soon as the prefix itself differs.

Corporate actions

B20 handles splits and reinvested dividends with a multiplier rather than by rewriting balances. That distinction drives how this package models price:

  • balanceOf, transfer and totalSupply stay raw, so pool reserves and the raw pool price are mechanically unaffected by a multiplier change. Raw OHLCV stays continuous across a split by construction.
  • The share-denominated price is the raw price divided by the multiplier in force. A 2:1 split halves it.

b20_multiplier_schedule records the timeline and b20_effective_multiplier(token, ts) resolves it. Two details matter:

  • A scheduled update names a future effectiveAt, and the flip is lazy — nothing is emitted when it actually takes effect. Resolve by timestamp, never by block.
  • The spec asks indexers to treat effects emitted outside an Announcement / EndAnnouncement bracket as emergency overrides. Every event carries in_announcement, and b20_unannounced_actions surfaces them.

Both multiplier generations are decoded: Beryl (live today) emits only the deprecated MultiplierUpdated, while Cobalt adds ERC-8056's UIMultiplierUpdated.

Metadata is mutable

Every one of the 13 tokenized stocks issued so far has been renamed at least once — AAPLc was created as AAPL and renamed at block 49,485,877. A symbol captured once at pool creation goes stale, so NameUpdated / SymbolUpdated are indexed and the b20_token_current view resolves the live value.

It indexes every B20, not just the stocks

B20 is a general RWA standard and detection here is a pure address-prefix check, so any B20 indexes automatically — stablecoins, commodities, memecoins, whatever ships next. Running map_b20_events over a single 10,000-block window on Base found 503 newly created B20 tokens, from 210 distinct deployers.

Almost none of them are stocks. That window was HouseCoin, ₿itcoin, BETTER COIN, Mortgage Coin — memecoins, 18 decimals, metadata keys twitter / telegram / image. 502 of the 503 were the asset variant.

Which kills the obvious classifier: filtering on b20_variant = 'asset' would have labelled 500 memecoins as tokenized stocks. The variant byte only separates asset from stablecoin, and the spec is explicit that Asset is general purpose — "assets of all kinds."

The real signal is the ISIN. Every Coinbase tokenized stock publishes one through ExtraMetadataUpdated (AAPLc → US0378331005, NVDAc → US67066G1040), and none of the 503 memecoins carried one. So the views form a ladder:

ViewContents10k-block sample
b20_poolsevery pool holding any B20all of them
b20_asset_poolsasset variant — tokenized anything502 of 503
stock_poolsasset variant with a registered ISINthe actual securities

b20_token_identity exposes isin, isin_country and is_security per token.

What B20-ness still does not prove: createB20 is permissionless — anyone can create a B20, name it anything, and write any isin they like into metadata. The prefix proves namespace, the ISIN proves intent, neither proves issuance. Confirm the issuer before presenting a pool as a given company's stock.

v1.0.0: Coinbase stocks on Aerodrome

Coinbase-issued tokenized stocks are B20 Asset tokens with day-one liquidity on Aerodrome (Aerodrome, 2026-08-24).

Registry (src/stocks.rs, src/stock_list.rs). A token is a stock when any of these holds:

  1. It is in the built-in list of 194 tokens generated by scripts/gen_stocks.py from the official Coinbase Tokenized Stocks API (GET https://api.coinbase.com/v1/tokenized-stocks, fetched 2026-10-21).
  2. Its B20Created transaction also carries a log from the Coinbase token factory 0x4bA3E29E254F25E94E61C4c9d67b37027331534D (/v1/tokenized-stocks/chains). New listings are picked up this way with no repackage.
  3. It is listed in the extra_stock_tokens param of map_pools / map_stock_swaps (0xaddr:TICKER,...).

A self-declared ISIN is not enough, because createB20 is permissionless.

ModuleOutput
store_stock_registryAuto-detected tickers, symbol renames, multiplier timeline
map_poolsAdds is_stock / stock_ticker
map_stock_swapsStock trades from both venues: side, raw and decimal amounts, multiplier, share amount, USD notional (native USDC quote only)
store_stock_daily_volumePer `day
map_gauge_eventsVoter GaugeCreated / GaugeKilled / GaugeRevived / Voted / Abstained

share_amount = stock_amount * uiMultiplier / 1e18. The multiplier is resolved at the swap's block timestamp (scheduled updates flip lazily at effectiveAt; cancellations are honoured), per B20 UI multipliers.

New SQL tables: stock_trades, stock_daily_volume, stock_corporate_actions, gauges, gauge_status_changes, gauge_votes, plus the coinbase_stock_pools and gauge_current views.

Other v1.0.0 changes:

  • Fixed a halt. v0.3.x panicked at block 13,847,696 (BigInt ... too large to fit into u64 in map_swaps), so full-history runs could not get past it. Conversions now saturate.
  • Fixed a SQL-sink break. Empty strings were written into NUMERIC columns (b20_events.amount, old_value, new_value, cancelled multipliers). They are now NULL.
  • Event-signature blockFilters on the pool and swap/liquidity/sync maps (via ethereum-common index_events) skip blocks with no relevant topic. Output is unchanged.
  • store_cl_volumes also tracks cl:pool:<addr>:volume1 (token1 side).

Architecture

sf.ethereum.type.v2.Block
│
├─► map_pools ─────► Pools               PoolCreated from 3 factories
│      └─► store_pools                   registry: tokens, venue, B20 flag
│
├─► map_swaps ─────► SwapEvents          v2, enriched from the registry
├─► map_liquidity ─► LiquidityEvents     v2 Mint / Burn
├─► map_syncs ─────► SyncEvents          v2 reserves
│
├─► map_cl_swaps ──► ClSwaps             Slipstream — tokenized stocks
├─► map_cl_liquidity ► ClLiquidityEvents Slipstream Mint / Burn / Collect
│
├─► map_b20_events ► B20Events           token events + multiplier timeline
│
└─► db_out ────────► DatabaseChanges     PostgreSQL / ClickHouse

Why every handler gates on the registry

Aerodrome inherits event signatures from the protocols it descends from, so a topic match alone does not mean the log came from an Aerodrome pool:

Sampling Base for each topic and checking every emitter against PoolFactory.isPool() (25 busiest emitters across three separate block windows):

EventAerodromeNot AerodromeContaminated
v2 Mint(address,uint256,uint256)121352%
v2 Burn7330%
v2 Sync2414%
v2 Swap2500%
Slipstream Swap——byte-identical to Uniswap V3's

Mint(address,uint256,uint256) is byte-identical to Uniswap V2's, so most of an ungated handler's rows come from contracts that are not Aerodrome at all. Membership in the factory-built registry is what actually identifies a pool, so every handler skips logs from pools it does not know — including Swap, which measures clean but has no reason to be an exception.

The registry starts at the v2 PoolFactory deployment, so it is complete. Raising initialBlock above that trades completeness for backfill speed — pools created before the new start are unknown, and their events are skipped.

Contract addresses (Base)

Every address below was verified by calling it on Base.

ContractAddressDeployed
v2 PoolFactory0x420DD381b31aEf6683db6B902084cB0FFECe40Dablock 3,200,559
Slipstream CLFactory0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809Ablock 13,843,704
Slipstream CLFactory0xaDe65c38CD4849aDBA595a4323a8C7DdfE89716ablock 36,953,918
Slipstream CLFactory0xf8f2eB4940CFE7d13603DDDD87f123820Fc061Efblock 44,394,724
FactoryRegistry0x5C3F18F06CC09CA1910767A34a20F771039E37C0block 3,200,576
Voter0x16613524e02ad97eDfeF371bC883F2F5d6C480A5
AERO0x940181a94A35A4569E4529A3CDfB74e38FD98631block 3,200,550
IB20Factory precompile0xB20f000000000000000000000000000000000000

Those four pool factories are exactly what FactoryRegistry.poolFactories() returns — the protocol's own list. Hardcoding a subset is the trap here: the newest CLFactory alone holds 2,184 pools, and every tokenized-stock CL pool at launch was created by 0xf8f2eB49…, so a listener watching one factory sees a fraction of Aerodrome.

map_factories watches the registry's Approve / Unapprove events, so a factory approved after this package was built registers itself and its pools index with no code change.

Quick start

substreams auth
substreams build

# Slipstream swaps, tokenized stocks included
substreams run substreams.yaml map_cl_swaps -e base -s 49273390 -t +5000

# Pool discovery — watch stock pools appear
substreams run substreams.yaml map_pools -e base -s 49273390 -t +250

# B20 token events: renames, corporate actions, halts
substreams run substreams.yaml map_b20_events -e base -s 49480000 -t +10000

Backfill cost

initialBlock is the v2 PoolFactory deployment (3,200,559) so the registry covers all of Aerodrome's history. To index tokenized stocks only, set every module's initialBlock to 44,394,724 (CLFactory v2). Every stock pool lives on that factory, and the backfill is roughly 10× shorter.

Stream to SQL

substreams-sink-sql setup "psql://postgres:password@localhost:5432/aerodrome?sslmode=disable" \
  aerodrome-substreams-v1.0.0.spkg
substreams-sink-sql run   "psql://postgres:password@localhost:5432/aerodrome?sslmode=disable" \
  aerodrome-substreams-v1.0.0.spkg

# ClickHouse
substreams-sink-sql setup "clickhouse://default:@localhost:9000/default" \
  aerodrome-substreams-v1.0.0.spkg --engine=clickhouse

Tables

TableContents
poolsPool registry with token metadata and the B20 flag
aerodrome_swapsv2 swaps
aerodrome_cl_swapsSlipstream swaps, with sqrtPriceX96 and tick
aerodrome_cl_liquiditySlipstream Mint / Burn / Collect
b20_eventsEvery B20 token event, flattened
b20_tokensB20Created birth certificates
b20_multiplier_scheduleCorporate-action timeline
stock_tradesv1.0.0: Coinbase-stock trades with shares and USD notional
stock_daily_volumev1.0.0: daily per-ticker USD / token / share volume
stock_corporate_actionsv1.0.0: multiplier changes by ticker
gauges, gauge_status_changes, gauge_votesv1.0.0: Voter gauges and votes

Views

ViewAnswers
b20_poolsEvery pool holding any B20 token
b20_asset_poolsAsset-variant B20 pools (memecoins included)
stock_poolsAsset-variant pools with a registered ISIN
b20_token_identityPer token: variant, symbol, ISIN, is_security
b20_swapsEvery B20 trade across both venues
stock_swapsTokenized-security trades, priced in the quote asset
stock_candles(interval)OHLCV at any bucket size (300 / 3600 / 14400 / 86400)
stock_pools_24h24h volume, trades, high / low per stock pool
b20_token_currentLive name and symbol, honouring renames
b20_corporate_actionsSplits and dividends, with the announcement that disclosed them
b20_unannounced_actionsChanges made outside an announcement bracket
b20_pause_historyTrading halts
cl_swaps_pricedAll Slipstream swaps with a decimal-adjusted price
-- What are the tokenized stocks doing today?
SELECT b20_symbol, trades_24h, volume_24h, high_24h, low_24h FROM stock_pools_24h;

-- Hourly candles for one stock pool
SELECT * FROM stock_candles(3600, '0xa3b1e3f9747065e2073722ff4c9027d3ea4994f0');

-- Any pending corporate action?
SELECT symbol, new_multiplier, effective_at FROM b20_corporate_actions WHERE pending;

Prices are derived from sqrtPriceX96 — the pool's post-trade price — rather than from an output/input ratio, which yields a price in one direction and its reciprocal in the other and makes a naive candle series alternate between the two.

token0_decimals / token1_decimals are -1 when the metadata call failed, so a missing value is never mistaken for 0 decimals. Price views return NULL rather than a wrong number in that case.

Development

cargo test          # unit tests
substreams build    # protobuf + ABI codegen + wasm
substreams pack     # .spkg

Event decoders are generated from abi/*.json by build.rs using substreams-ethereum's Abigen, so a topic0 cannot drift from the deployed contract the way a hand-written matcher can.

abi/
  pool.json          Aerodrome v2 Pool
  pool_factory.json  Aerodrome v2 PoolFactory
  cl_pool.json       Slipstream CLPool
  cl_factory.json    Slipstream CLFactory
  b20.json           Base B20 token
  b20_factory.json   IB20Factory precompile
  erc20.json         decimals() / symbol()
  voter.json         Voter events (subset of IVoter.sol)

ABIs were taken from the official sources — aerodrome-finance/contracts, aerodrome-finance/slipstream and base/base-std — and every topic0 was confirmed against live Base logs.

Modules

Execution graph

21 modules
store

store_cl_volumes

from #3200559

Store value

bigint

Update policy

add

store

store_factories

from #3200559

Store value

int64

Update policy

set

store

store_pool_stats

from #3200559

Store value

bigint

Update policy

add

Inputs

store

store_stock_daily_volume

from #3200559

Store value

bigdecimal

Update policy

add

store

store_stock_registry

from #3200559

Store value

string

Update policy

set

store

store_swap_volumes

from #3200559

Store value

bigint

Update policy

add

Inputs

store

store_unique_traders

from #3200559

Store value

string

Update policy

set_if_not_exists

Inputs

Show 2 dependencies
map

ethcommon:all_events

`all_events` gives you all the events in a block (from successful transactions), with basic block hash/number/timestamp and transaction hash

Output

sf.substreams.ethereum.v1.Events

blockIndex

ethcommon:index_events

`index_events` sets the following keys on the block: * Event signatures evt_sig:0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef (signature in hex, prefixed by 0x) * Event address evt_addr:0x0123456789abcdef0123456789abcdef01234567 (address in hex, prefixed by 0x)

Output

sf.substreams.index.v1.Keys