Lesson 32 · One Verifier In Depth · Quality Internals

Conservation of supply

Σ HOLDS ≈ totalSupply — and the judgment hidden in one "simple" check. ~12 min.

Builds on: L29 · L2 Anchor: ERC-20 totalSupply, dust holders New: asymmetric tolerance New: precision placement

L29 gave you the verifier framework. Now we open one — the simplest-sounding of the ~30 — and watch how much engineering judgment a one-line invariant actually demands. The invariant is something you could write on a napkin: the sum of everyone's balance equals the total supply. Making that a trustworthy production signal is where the real lesson is.

Your anchor: mass conservation for an ERC-20
Every unit of an ERC-20 that exists is held by someone: Σ balanceOf(holder) == totalSupply(). In the graph, each holder is a HOLDS edge carrying quantity_raw (the holder's raw balance). So the graph's HOLDS edges for a token should sum to its on-chain totalSupply(). Store more than exists and you've double-counted; store less and you're missing holders. That's the whole check — Σ HOLDS.quantity_raw ≈ totalSupply(token).

1 · The shape: a pure-drift verifier

Recall L29's gap / excess / drift taxonomy. This verifier has no gaps or excess by construction: EnumerateOnChain and EnumerateGraph return the same set — the focus tokens — because a token's identity (its address) trivially exists on both sides. So coverage is 100% always, and all the signal lives in VerifyInstance, which forms the ratio. It's a clean reminder that not every verifier uses all three finding kinds — some are pure value-checks.

func VerifyInstance(ctx, ref, _) (pass bool, diffs []Diff, err error) {
    storedQty   := v.sumStoredHolds(ctx, ref.ID)              // Σ HOLDS.quantity_raw from graph
    chainSupply := enrichment.ReadERC20TotalSupply(ctx, ref)  // totalSupply() from chain
    return evaluateConservation(ref, storedQty, chainSupply)
}

Scope is focus tokens only (is_focus_token=true). Running this on LINK — which the indexer tracks at ~19% coverage by design — would just flag intentional sparsity as a violation. The invariant only applies where coverage is meant to be high.

2 · The asymmetric tolerance band — the real idea

L29's drift tolerance was symmetric (abs ∧ rel). This verifier's is deliberately lopsided, and the asymmetry encodes a design decision about the indexer itself. With ratio = Σstored / totalSupply:

FAIL
PASS
FAIL
0.70
1.02
0
BoundValueWhy this number
Upper1.02 (tight)Overshooting totalSupply is physically impossible — you can't hold more than exists. >1.02 is a real bug, e.g. counting a receipt token's units against the underlying's supply. A 2% sliver only absorbs rounding.
Lower0.70 (loose)Undershooting is expected — the indexer intentionally skips enormous dust tails (holders below the monitored-set threshold). Only <0.70 means a catastrophic-loss class. USDC legitimately sits at 0.77.
The threshold IS the coverage policy
That asymmetry isn't a fudge factor — it's the indexer's monitored-set policy expressed as a number. "We track the whales, not the dust" means "stored supply will land somewhere in [0.70, 1.00], and that's correct." A symmetric band would either reject healthy tokens like USDC (too tight below) or miss real double-counting (too loose above). Read the constants and you've read the design decision.

3 · Precision placement — big.Int where it counts

An 18-decimal token with a multi-billion supply has raw balances around 290. float64 loses integer precision past 253, so summing balances as floats would silently corrupt the numerator. The verifier is careful about where it spends precision:

// quantity_raw is stored as a STRING (uint256-safe). Sum with arbitrary precision:
sum := new(big.Int)
for _, s := range rawQuantities {
    b, ok := new(big.Int).SetString(s, 10)   // exact, no overflow
    if ok { sum.Add(sum, b) }
}
// …but the FINAL ratio is float64 — acceptable, because we only test a 1–2% band:
ratio := storedF / chainF        // ~1e-7 worst-case error, vs a 0.02 threshold
Exactness in the sum, float in the ratio
This is the same instinct as L19's compensated KeyedSum: be exact where error accumulates (summing thousands of uint256 balances), and accept cheap float where the result is a coarse comparison (a ratio against a 2% band). It also compares in raw units, never decimals-scaled — both sides come from the same raw source, so scaling would only introduce precision loss for no benefit.

4 · Skip, don't lie — observability over false fails

What if totalSupply() can't be read? You can't form a ratio without a denominator. The verifier skips rather than fails, and — crucially — records why on a closed, dashboardable label set:

ConditionOutcomeReason label
totalSupply() unreadable (RPC failed)skip (pass-by-skip)chain_supply_unreadable
totalSupply() == 0skip (suspicious but not a hard fail; some upgrades zero supply briefly)chain_supply_zero
stored qty missingfail — the indexer claiming a tracked token has no balances is its own bug
A skip is loud, not silent
The asymmetry here matters: a chain-read failure skips (it's not the graph's fault, and a permanently-broken totalSupply() shouldn't fail forever), but a missing stored balance fails (that's a real graph bug). Every skip increments a metric with its reason, so "we couldn't check" never masquerades as "we checked and it's fine" — the same honest-gap discipline as L20's honest-zero and L25's discovery_gap.
How much lives in one verifier
A napkin invariant became: a pure-drift verifier scoped to focus tokens, an asymmetric band that encodes the monitored-set policy, big.Int sums with a float ratio, and skip-with-a-reason observability. Multiply that judgment by ~30 verifiers and you have the quality harness. Simple invariant, serious engineering.

Check yourself

1. What on-chain invariant does this verifier enforce for a focus token?
2. Why does this verifier produce only drift findings, never gaps or excess?
3. Why is the tolerance band asymmetric — tight above (1.02), loose below (0.70)?
4. USDC's ratio sits around 0.77 and the verifier passes it. What does that 0.77 represent?
5. Why are the HOLDS quantities summed with big.Int rather than float64?
6. The final ratio is computed in float64 even though the sum was exact. Why is that acceptable?
7. totalSupply() returns an RPC error this cycle. What does the verifier do?
8. An unreadable chain supply skips, but a missing stored quantity fails. What distinguishes the two?
↳ Ask your teacher
Try: "Show me ReadERC20TotalSupply and how it returns nil on revert." · "How does the runner sample which focus tokens to verify each cycle?" · "What exactly inflates the numerator past 1.02 — a concrete double-count bug?" · "How does a drift here become a Diff the /quality page renders?" · "Could a receipt-token healer fix an overshoot, or is it a code bug?"

What you can now do

Grounded in: pkg/quality/chainref/verify_balance_conservation.go (BalanceConservationVerifierΣ HOLDS.quantity_raw ≈ totalSupply, pure-drift / same C=G enumerate, focus-token scope, conservationLowerBound=0.70 / conservationUpperBound=1.02 asymmetric band, sumStoredHolds big.Int over string quantity_raw, evaluateConservation float ratio, conservationSkipReason chain_supply_unreadable / chain_supply_zero, recordSkip telemetry), enrichment.ReadERC20TotalSupply. Verify against source — the code is the truth.