Single sink module. Merges flows, observations and first-sight VaultMeta rows into vaultflows.v1.Events.
Output
vaultflows.v1.EventsPackage ref
erc4626-flows@v0.1.0Run package
CLI
Run map_events from the command line.
substreams run erc4626-flows@v0.1.0 map_events -e basesubstreams auth or directly on thegraph.market (see docs).README
ERC-4626 deposit/withdraw flows and sampled share-value observations on Base (chain id 8453), emitted as
vaultflows.v1.Events for the ClickHouse from-proto sink. Built on Pinax's erc4626 package (raw
Deposit / Withdraw logs matched by signature across every address on the chain).
Status: v0.1.0, compiled in CI (.github/workflows/build-substreams.yml) and locally, and run against
base-mainnet.streamingfast.io:443 (evidence in runs/live/) and into local ClickHouse through
substreams-sink-sql from-proto (docs/build/sink-spike.md).
Single sink module map_events → vaultflows.v1.Events. Each repeated field is one ClickHouse table
(DDL is generated by the sink from the proto annotations; nothing is hand-written).
| Field | Table | One row per | Notes |
|---|---|---|---|
vault_flows | vault_flows | Deposit/Withdraw log on a configured vault | caller, owner, receiver kept separate; raw amounts as decimal strings; normalized amounts and execution_rate (assets per share implied by this one flow, 18 fractional digits) only when meta_valid. direction is the string deposit or withdraw (plain String column, not an enum: substreams-sink-sql 4.13.1 from-proto panics on a populated proto3 enum field). |
share_value_observations | share_value_observations | configured vault × sampled block (block_number % interval == 0) | convertToAssets(10^shareDecimals), totalAssets(), totalSupply() read at that block's state. Failed vaults are still emitted with call_ok=false. Never interpolated. |
vaults | vaults | address, on first sight | First-sight probe result: asset, decimals, name, symbol, compliant, in_configured_list, call_ok / call_error. Chain-wide, not only configured vaults. |
share_transfers | share_transfers | — | Empty in v0.1.0 (share migration is a "should"). |
Row ids are deterministic ({chain_id}-{block_number}-{log_index}, {chain_id}-{block_number}-{vault},
{chain_id}-{vault}), so replays and restarts are idempotent. Numeric string columns are never empty: "0" plus
the accompanying meta_valid / call_ok flag means "not computable".
erc4626:map_events (import) ─► store_vault_seen ─deltas─► map_vault_probe ─► store_vault_meta ─get─► map_flows ─┐
sf.substreams.v1.Clock + params ─► map_share_value_observations ────────────────────────────────────────────────┼─► map_events
map_vault_probe (first-sight VaultMeta rows) ─────────────────────────────────────────────────────────────────┘
| Module | Kind | Inputs | Purpose |
|---|---|---|---|
store_vault_seen | store, set_if_not_exists int64 | erc4626:map_events | Key per vault address; its CREATE delta is the one-shot "first sight" signal. |
map_vault_probe | map | params, Clock, store_vault_seen (deltas) | On first sight: batch asset(), decimals(), totalAssets(), name(), symbol(), then asset.decimals() and convertToAssets(10^shareDecimals). Two RPC round trips per new vault, ever. |
store_vault_meta | store, set_if_not_exists proto | map_vault_probe | Cache of the probe result. |
map_flows | map | params, Clock, erc4626:map_events, store_vault_meta (get) | VaultFlow rows for configured vaults. Non-compliant vaults produce no rows. |
map_share_value_observations | map | params, Clock | Sampled reads; two RPC round trips per sampled block for the whole list. |
map_events | map | the three maps above | Merge. Sink this module. |
All modules have initialBlock: 51001200 (see specs/streamsmith.yaml). Metadata reads use Substreams'
eth_call, which executes at the hash of the block being processed.
One urlencoded string, identical for the three parameterized modules (manifest params:; override with -p):
vaults[]=0x050ce30b927da55177a4914ec73480238bad56f0&vaults[]=0xbeef0e0834849acc03f0089f01f4f1eeb06873c9&interval=1800&chain_id=8453
| Key | Meaning |
|---|---|
vaults[] (repeatable; also vaults= comma list) | Vault addresses. Any case; normalized to lowercase 0x. |
interval | Sampling interval in blocks (>= 1). 1800 × 2 s = 1 h on Base. |
chain_id | Optional, default 8453. Stamped on every row and id. |
Unknown keys, a missing/zero interval or an empty vault list fail the module with a clear error.
assets include any entry fee and the Withdraw assets are net of any
exit fee, so deposit-implied and withdraw-implied execution rates differ by the vault's fee spread. Compare a
flow's execution_rate with the vault's sampled share value, not deposits with withdrawals.convertToAssets in the last digits. Normal.compliant means "these five calls returned and decoded at first sight". A contract
can pass the probe and still not be an ERC-4626 vault; a vault can fail it at one block (e.g. paused) and be
recorded non-compliant for good (the cache is write-once).total_assets_raw / total_supply_raw are point-in-time reads of two view
functions and are not a statement about anything held elsewhere.vaults[] to see them; the observation module should keep sampling the underlying vaults.Deposit/Withdraw with the same topic0; that is why
the probe exists and why vault_flows is limited to the configured list.imports:
vaultflows: https://<host>/erc4626-flows-v0.1.0.spkg # or the substreams.dev URL once published
modules:
- name: my_module
kind: map
inputs:
- map: vaultflows:map_events
output:
type: proto:my.pkg.Output
Or re-export a module unchanged with use::
- name: flows_only
use: vaultflows:map_flows
Override params per module: substreams run … -p map_flows="vaults[]=0x…&interval=1800".
# native tests of the pure functions (params, normalization, execution-rate math, ids)
cargo test
# wasm + spkg (needs rustup target wasm32-unknown-unknown, buf, substreams CLI 1.22.x)
substreams build
substreams info erc4626-flows-v0.1.0.spkg
# 200-block gate range with known activity on both vaults
# --limit-processed-blocks 0 is required: the CLI refuses a request whose store preparation exceeds 10,000 blocks
substreams run -e base-mainnet.streamingfast.io:443 erc4626-flows-v0.1.0.spkg map_events \
--network base -s 51092254 -t +200 -o jsonl --limit-processed-blocks 0
docs/build/substreams-facts.md (b)).Modules
Single sink module. Merges flows, observations and first-sight VaultMeta rows into vaultflows.v1.Events.
Output
vaultflows.v1.EventsOne VaultFlow per Deposit/Withdraw on a vault in the configured list. Raw amounts as decimal strings; normalized amounts and execution_rate only when the cached metadata is valid. Only `vault_flows` is populated.
Output
vaultflows.v1.EventsInputs
When block_number % interval == 0, batch convertToAssets(10^shareDecimals), totalAssets(), totalSupply() for every configured vault at this block's state. Failed vaults are still emitted with call_ok=false. Only `share_value_observations` is populated.
Output
vaultflows.v1.EventsInputs
For each vault seen for the first time (CREATE delta), batch eth_call asset(), decimals(), totalAssets(), name(), symbol(), then asset.decimals() and convertToAssets(10^shareDecimals). Emits one VaultMeta row per new vault (compliant=false when any required probe fails). Only field `vaults` of Events is populated.
Output
vaultflows.v1.EventsInputs
Caches the probe result per vault (key = lowercase 0x address). Written once, ever.
Store value
proto:vaultflows.v1.VaultMetaUpdate policy
set_if_not_exists
Inputs
Marks every address that emitted an ERC-4626 Deposit or Withdraw. set_if_not_exists means a key is created exactly once, so the deltas of this store are the "first sight" signal.
Store value
int64
Update policy
set_if_not_exists
Inputs
Output
erc4626.v1.Events