All packages
erc4626-flows

erc4626-flows

pavvann
v0.1.0/65 downloads/Repository

Package ref

erc4626-flows@v0.1.0

Run package

CLI

Run map_events from the command line.

substreams run erc4626-flows@v0.1.0 map_events -e base
Authenticate by running substreams auth or directly on thegraph.market (see docs).

README

erc4626-flows

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).

What it emits

Single sink module map_eventsvaultflows.v1.Events. Each repeated field is one ClickHouse table (DDL is generated by the sink from the proto annotations; nothing is hand-written).

FieldTableOne row perNotes
vault_flowsvault_flowsDeposit/Withdraw log on a configured vaultcaller, 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_observationsshare_value_observationsconfigured 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.
vaultsvaultsaddress, on first sightFirst-sight probe result: asset, decimals, name, symbol, compliant, in_configured_list, call_ok / call_error. Chain-wide, not only configured vaults.
share_transfersshare_transfersEmpty 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".

Modules

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) ─────────────────────────────────────────────────────────────────┘
ModuleKindInputsPurpose
store_vault_seenstore, set_if_not_exists int64erc4626:map_eventsKey per vault address; its CREATE delta is the one-shot "first sight" signal.
map_vault_probemapparams, 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_metastore, set_if_not_exists protomap_vault_probeCache of the probe result.
map_flowsmapparams, Clock, erc4626:map_events, store_vault_meta (get)VaultFlow rows for configured vaults. Non-compliant vaults produce no rows.
map_share_value_observationsmapparams, ClockSampled reads; two RPC round trips per sampled block for the whole list.
map_eventsmapthe three maps aboveMerge. 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.

Params

One urlencoded string, identical for the three parameterized modules (manifest params:; override with -p):

vaults[]=0x050ce30b927da55177a4914ec73480238bad56f0&vaults[]=0xbeef0e0834849acc03f0089f01f4f1eeb06873c9&interval=1800&chain_id=8453
KeyMeaning
vaults[] (repeatable; also vaults= comma list)Vault addresses. Any case; normalized to lowercase 0x.
intervalSampling interval in blocks (>= 1). 1800 × 2 s = 1 h on Base.
chain_idOptional, 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.

Caveats (read before quoting numbers)

  • Fee spread. Per EIP-4626 the Deposit 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.
  • Virtual offset / rounding. Vaults with a virtual share offset (OpenZeppelin-style) round in the vault's favour; one flow's execution rate can differ from convertToAssets in the last digits. Normal.
  • Heuristic validation. 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).
  • Observed-window only. Observations exist only at sampled blocks inside the streamed range. Any growth figure derived from them is "over the observed window, blocks X–Y" and nothing more. Nothing is interpolated.
  • No aggregate-value claims. total_assets_raw / total_supply_raw are point-in-time reads of two view functions and are not a statement about anything held elsewhere.
  • Fee wrappers. Privy-executed flows land on wrapper contracts, not on the vaults above. Add the wrapper addresses to vaults[] to see them; the observation module should keep sampling the underlying vaults.
  • Same-signature events. A non-4626 contract can emit a Deposit/Withdraw with the same topic0; that is why the probe exists and why vault_flows is limited to the configured list.

Compose it

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".

Build and run

# 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

Unverified

  • Behaviour on the hosted runner (this package has only been streamed from the CLI and sunk to a local ClickHouse container).
  • Whether the endpoint sets an eth_call "fallback to latest" window for old blocks (would silently break historical observations; see docs/build/substreams-facts.md (b)).

Modules

Execution graph

7 modules
map

map_flows

One 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.

from #51001200

Inputs

paramssf.substreams.v1.Clockerc4626:map_eventsstore_vault_meta
map

map_share_value_observations

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.

from #51001200

Inputs

paramssf.substreams.v1.Clock
map

map_vault_probe

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.

from #51001200

Inputs

paramssf.substreams.v1.Clockstore_vault_seen
store

store_vault_seen

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.

from #51001200

Store value

int64

Update policy

set_if_not_exists

Show 1 dependency