A tiny package, a dense lesson: containing corruption with a single un-bypassable invariant. ~10 min.
Builds on: L9 · L32 · L2Anchor: uint256 raw balancesNew: gateway invariantNew: corruption containment
A change of scale: after whole subsystems, here's a single 150-line file. But it's worth a lesson because it
solves a bug class you'll meet anywhere there's a cache — corruption that spreads because the cache trusts whatever it's
handed — and the fix is a pattern worth internalizing: make one invariant un-bypassable by making the package the only
door.
Your anchor: a raw balance has a shape
An ERC-20 raw balance is an unsigned uint256 — never negative, and in practice nowhere near the top of its range
(even a 10-trillion-supply token at 24 decimals is only ~124 bits). So a cached "balance" that is negative, or sits at
BitLen = 256, is definitionally not a balance — it's garbage. That observation is the whole invariant.
1 · The bug class: sticky cache corruption
The hot cache balance:{chain}:{token}:{holder} in Redis speeds up balance lookups. Before this package, any
writer could put anything in it — and several did: a pre-floor-fix indexer path, a reconciler that piped through a quirky
RPC return, a genesis rebuild that propagated already-corrupt Neo4j quantity_raw. Each wrote a 2^256 − X
garbage string. Then the corruption became sticky:
garbage write → next batch reads it as a SEED → re-writes ON TOP → garbage persists
The cache had no notion of "valid," so once garbage was in, it propagated forward and survived every cycle. (Observed
on stage, 2026-05-19.) That's the failure this package exists to close.
2 · The fix: one typed gateway, one invariant
pkg/balancecache is the single typed gateway for that Redis key. Every read and write goes through it,
and it enforces one predicate — IsPlausible:
reject with a WARN + ErrImplausibleBalance; skip the write (don't retry — it won't become plausible). SetMany returns a skipped count.
Read
treat as a cache miss — drop through to the fallback (re-fetch from chain). The same predicate is shared with the indexer's seed path, so read and write can never disagree on "valid."
Un-bypassable by construction
The point isn't just "validate inputs" — it's where. By making this package the only door to the key, the import
graph itself forces every writer through the check. You can't write the cache without calling Set, and
Set can't be made to store garbage. The invariant holds not by everyone remembering to check, but because there's
no other path. That's the same one-door discipline as the single writer (L9), applied to a cache.
3 · The threshold: calibrate for safe-side margin
Why MaxBalanceBits = 240 specifically? A small calibration story worth keeping:
Value
BitLen
10-trillion-supply token @ 24 decimals (~1e37)
~124
pathological 1e54 RWA tokenisation
~180
the observed corruption class
256
Put the line where nothing legit can reach it
The corruption sits at 256; any real balance is well under 200. So the threshold has enormous room — and they deliberately
chose 240 over an earlier 200: both reject the 256-bit garbage identically, but 240 leaves 60+ bits of margin
above any conceivable real token, so it can never reject a legitimate value. When the bad case is far from the good
case, set the line to maximize the safe-side margin — the same anti-false-positive instinct as L32's loose lower bound.
4 · Two smaller choices
No TTL — durable by design.Set writes with no expiry (SET key val 0). Balances don't "go stale and vanish"; they're refreshed by being overwritten with fresh reads (recall L26's price-dirty recompute). Eviction is the operator's job via Redis maxmemory policy, not a per-key TTL.
Mandatory source on writes.Set requires a non-empty source string, logged on rejection — so when an implausible value is caught, the WARN names which writer produced it, pointing straight at the upstream bug. Provenance for debugging, the same instinct as the healers' source stamps (L30).
The transferable lesson
Three ideas in 150 lines: (1) a cache that trusts its inputs turns one bad write into permanent corruption;
(2) the cure is a single typed gateway whose invariant the import graph makes un-bypassable, with the same
predicate on read and write; (3) when garbage is far from legit, calibrate the threshold for maximum safe-side
margin. None of it is balance-specific — it's how you defend any shared mutable cache.
Check yourself
1. What made the pre-gateway cache corruption "sticky" rather than self-correcting?
2. IsPlausible rejects a value that is negative or exceeds 240 bits. Why are those "definitionally not a balance"?
3. How does the package guarantee no writer can bypass the plausibility check?
4. On the read side, what happens when a cached value fails IsPlausible?
5. The corruption sits at BitLen 256 and any real balance is under ~200. Why set the threshold at 240 rather than 200?
6. The cache is written with no TTL. How do stale balances get corrected, then?
7. Why does Set require a non-empty source string?
8. Which earlier principle does the gateway most directly echo?
↳ Ask your teacher
Try: "Show me seedPreBatchBalance and how it shares IsPlausible." ·
"What other writers call Set / SetMany today?" ·
"How does the indexer fall back when a read misses?" ·
"Could the corrupt Neo4j quantity_raw still seed a bad value despite this?" ·
"Why a 60-bit margin and not, say, exactly at 200?"
What you can now do
Explain why a trusting cache turns one bad write into sticky, self-propagating corruption.
State the plausibility invariant (non-nil, non-negative, ≤ 240 bits) and why those shapes can't be real balances.
Explain the single-typed-gateway pattern and how the import graph makes the invariant un-bypassable.
Explain read-as-miss vs write-as-reject sharing one predicate, and the threshold's safe-side-margin calibration.
Explain the no-TTL durability choice and the mandatory source for rejection provenance.
Grounded in: pkg/balancecache/balancecache.go (single typed gateway for balance:{chain}:{token}:{holder}, IsPlausible non-nil/non-negative/BitLen≤MaxBalanceBits=240, Set/SetMany reject + WARN + ErrImplausibleBalance + skipped-count, read-as-miss shared with seedPreBatchBalance in pkg/indexer, no-TTL SET key val 0, mandatory source; the 2026-05-19 sticky-corruption bug class). Verify against source — the code is the truth.