BAMFS
Reference

Batched reads and writes

Multicall on the write side, index pagination on the read side, and the gas ceilings that bound both.

Multicall

Every core store inherits Solady's Multicallable, exposing multicall(bytes[]) → bytes[], which delegatecalls each inner call back to the same contract.

Uploading a 50-chunk file would otherwise mean 50 wallet prompts and 50 × 21,000 gas of transaction base cost. Instead:

  • One signature, one mempool entry, one receipt.
  • Atomic — any inner revert unwinds the whole bundle.
  • msg.sender is preserved, so per-op authorization behaves identically inside and outside a bundle.

Per-contract only

There is no cross-store router. Flows that span stores — createFile then publishVersion — are orchestrated client-side by the SDK, which keeps each store's auth surface narrow and its reasoning local.

The same reason makes a tagged publish two transactions: the CID lands on the ABX collection and the tag on the hook, and no single-contract multicall spans two contracts.

Not payable in practice

The Solady ABI marks multicall payable for compatibility, but every BAMFS write function is non-payable, so attaching value always reverts.

The gas ceiling is the node's, not the chain's

The SDK packs same-store ops under a per-bundle gas budget so a 500-chunk upload becomes a handful of transactions rather than 500.

That budget defaults to 24,000,000 — but it is then clamped, because eth_estimateGas caps at 224 (16,777,216) on every Base Sepolia endpoint tried, including keyed ones. That is a node setting; Base Sepolia's block gas limit is 1.2 billion. A bundle whose estimate exceeds the cap does not merely cost more — it fails to estimate at all, so the clamp applies regardless of which RPC you point at.

Tune with --max-bundle-gas, or --no-bundle to send one transaction per write when diagnosing a failure.

Pagination

Every list-shaped view has a matching triple:

xCount() → uint256
getXAt(uint256 i) → T
getXRange(uint256 start, uint256 end) → T[]

covering version history, tags, owned projects, directory listings, and chunk hashes. Clients walk arbitrarily large collections at fixed eth_call cost per page, and the unpaginated views still work.

Indexes, not opaque cursors

Ordering is append-only, so an index is a stable cursor: new entries append above it and never renumber what is below. That is what makes it safe to persist one across sessions.

let cursor: bigint | undefined;
do {
  const page = await readPublishPage(client, contracts, { limit: 20, before: cursor });
  render(page.records);
  cursor = page.nextCursor;
} while (cursor);

The one thing an index cursor cannot absorb is removal. Tags can be removed, so a walk that spans a removeTag may skip or repeat an entry. Version history and the publish ledger are append-only and have no such case.

Reads have a ceiling too

Every BAMFS read is an eth_call, and resolving a 1 MB file out of contract storage costs roughly 72M gas. Public endpoints differ by more than an order of magnitude in what they will execute, so the endpoint choice is load-bearing rather than cosmetic — see Deployments.

Pagination is what keeps a read under that ceiling: fetch a range, not a collection.

On this page