Proven Balances (Cross-Chain)
A ChainRender app can show a token balance that is proven, not just reported. The balance is checked against the chain's own state commitment — block header → state root → token account → the exact storage slot that holds the balance — by verifier contracts that are themselves on-chain, ownerless and immutable. The render then says precisely what was proven, when, and how strongly:
1000000.00 CRPD · proven @ block 11855669 · 24s old · Grade A (on Sepolia)
The same balance can also be proven on another chain, without bridging anything. On Robinhood Chain Testnet, whose parent chain is Sepolia, the line reads:
1000000.00 CRPD · proven @ block 11857149 · 781s old · Grade A (on Robinhood Testnet · instant upgrades, 1-of-2 Safe, as of 2026-10)
Milestone 1 is the proof stack on Ethereum Sepolia, proving balances of tokens whose home is Sepolia.
Milestone 2 delivers Sepolia block hashes to Robinhood through Robinhood's own parent-chain messaging, and the
same cache contract, unchanged, proves Sepolia balances there (see On another chain below).
Milestone 3 extends this to tokens ChainRender did not create — any existing ERC-20, proxies included — by
having the token's own balanceOf run on Sepolia and proving the answer (see Tokens ChainRender didn't create):
10000.00 USDC 0x1c7d4b196cb0c7b01d743fbc6116a902379c7238 · balanceOf @ block 11859963 (in-transaction) · 798s old · Grade A (on Robinhood Testnet · instant upgrades, 1-of-2 Safe, as of 2026-10)
Status: live on Sepolia and Robinhood Chain Testnet, alpha. Every contract is source-verified: Milestone 1 on Etherscan; Milestone 2 on Etherscan (the Sepolia sender) and on Robinhood's Blockscout explorer (the four Robinhood contracts). Milestone 2 ran its end-to-end acceptance on 2026-10-06, Milestone 3's snapshot mode (three more contracts, all source-verified) the same evening, and its storage-layout mode (eleven deployments, all source-verified) on 2026-10-07. Addresses: Deployed Contracts, Proven balances.
Why prove a balance at all
An ordinary dapp shows a balance by calling balanceOf and trusting the answer. On the token's own chain that's
fine. Everywhere else it isn't: another chain can't call the token, so the number has to come from somewhere — a
bridge, an oracle, an indexer — and the user is trusting that somewhere.
ChainRender's rule is that an address is a pointer, not a trust claim. So instead of trusting a reported number, the proof stack checks it against the source chain's own commitments, and the trust grade says which commitment it checked against.
How it works
Three steps, two of them permissionless writes and one a free read:
- Register the token (once). Anyone submits a proof of the home factory's record for the token. The record binds the token's definition (name, symbol, decimals, creator, supply), its runtime code hash and its storage namespace. The cache never asks the token to describe itself; every fact comes from the factory's proven storage.
- Refresh a balance. Anyone submits a proof of a holder's balance at some block. The cache verifies the header against the root source, proves the token account (its code hash must equal the registered one), then proves the balance slot computed from the registered namespace. The verified value is cached with its block, the proven header's timestamp and a grade.
- Render (free). The
xchain-balanceresolver is a plainview. It reads the cache and renders the value, its block, its age in seconds, its grade and the host chain. Proof verification never runs inside the render — a render has a small gas budget, and a proof costs hundreds of thousands of gas.
Verification is strict and fails closed. A tampered proof node, a header from another block, extra proof
elements, a non-canonical encoding or a token whose code differs from the record all revert — nothing can fail
toward a wrong number. A holder with no balance is proven too (an exclusion proof), and renders 0.
Root sources and grades
A proof is only as strong as the block hash it's checked against. That hash comes from a root source, and each root source sets a ceiling on the grade:
| Root source | Where the block hash comes from | Grade ceiling |
|---|---|---|
| Same-chain | the chain's own BLOCKHASH (last 256 blocks) and its EIP-2935 history contract (about 27 hours on Sepolia) |
A |
| Parent-chain retryable (Arbitrum) | a parent-chain block hash delivered through the rollup's own Inbox, accepted only from one fixed sender | A, relative to the host — always shown with the host's facet |
| Relay | posted by a fixed set of posters | C — a poster could post anything |
The cache keeps a separate entry per root source, so a weak source can never overwrite or block what a strong one proved. The resolver shows the highest-grade fresh entry (ties go to the newest).
Grade A means relative to the host chain, the chain the render runs on. On Sepolia, the value is exactly what Sepolia's consensus committed to at that block. On another chain it also trusts that chain, which is why the host's own trust is printed next to the letter (see below). It is the same letter as the trust grade, with the same discipline — the grade never claims more than the code checks.
What it does not establish
Every grade states what it does not cover. The resolver publishes this list on-chain (the scope-disclosure
kind renders it for any contract that implements it):
- anything beyond the host chain's own consensus (host-chain grade)
- the balance now — only as of the proven block
- anything the cache or its root source does not establish (see those)
That second line is why the render always carries the block and the age: a proven balance is a fact about a block, not a promise about the present.
Freshness
Age is measured in seconds since the proven header's timestamp, against the host chain's current block time.
The app chooses how old is too old (maxAgeSeconds). Past that, the render says
stale (proven @ block N · Grade G) — never the old number as if it were current. An entry dated more than
maxAgeSeconds into the future is not credible and renders unavailable.
Using it in an app
Bind the xchain-balance resolver kind in a data slot. Its arguments:
| Arg | Type | Meaning |
|---|---|---|
token |
address |
the home token (must be registered in the cache) |
holder |
address |
whose balance |
maxAgeSeconds |
uint64 |
older than this renders stale (…) |
fracDigits |
uint8 |
fractional digits to show (0–77) |
The resolver's cache is fixed at deployment, so a slot can never be pointed at a self-made cache that claims Grade A. Symbol and decimals come from the proven definition, not from the token. The renders it can produce:
| Situation | Render |
|---|---|
| a fresh proven entry | <amount> <SYMBOL> · proven @ block N · <age>s old · Grade <G> (on <host>) |
| … on a non-home chain | <amount> <SYMBOL> · proven @ block N · <age>s old · Grade <G> (on <host> · <facet>) |
| entries exist, none fresh | stale (proven @ block N · Grade G) |
| … on a non-home chain | stale (proven @ block N · Grade G · on <host> · <facet>) |
| token not registered, no entry, bad arguments, or out-of-bounds display values | unavailable |
Symbols are restricted to [A-Za-z0-9]{1,11} and decimals to at most 77, enforced when a token is created and
checked again at every step, so a token can't smuggle separators, markup or a fake "Grade A" into the line.
Proving and refreshing from code
The proof helpers live in the repository's chainc/xchain.ts. They fetch eth_getProof and the block header from
any RPC and package them for the cache; the header is RLP-encoded locally and checked against the real block hash
before anything is submitted.
import {
buildRecordProof, buildBalanceProof, waitUntilProvable,
KIND_XCHAIN_BALANCE, encodeXchainBalanceArgs,
} from "./chainc/xchain.js";
// a same-chain root for block n exists only once block n + 1 does
const n = receipt.blockNumber;
await waitUntilProvable(publicClient, n);
await cache.write.registerToken([sameChainSource, n, registration, await buildRecordProof(publicClient, factory, token, n)]);
const m = (await publicClient.getBlockNumber()) - 1n;
await cache.write.refresh([sameChainSource, m, token, holder, await buildBalanceProof(publicClient, token, holder, m)]);
// free: the exact string a ChainRender app shows
const line = await resolver.read.resolve([KIND_XCHAIN_BALANCE, encodeXchainBalanceArgs(token, holder, 3600n, 2)]);
scripts/xchain/deploy-sepolia.ts runs this whole sequence end to end (with XCHAIN_DEMO=1), and the chainc
mirror renderXchainBalance produces the identical string off-chain — a drift test keeps the two equal.
On another chain: Robinhood Chain Testnet
Robinhood Chain Testnet is an Arbitrum chain whose parent is Sepolia (checked on-chain: its rollup contracts live on Sepolia and its blocks track Sepolia's head). That gives it a way to learn Sepolia block hashes that adds no trust beyond Robinhood's own:
- Deliver (anyone, paid by whoever wants a fresh root).
ArbRootSenderon Sepolia reads a recent block hash from the Milestone 1 same-chain source and sends it to Robinhood as a retryable ticket through the rollup's Inbox. The sender is ownerless and holds nothing; the caller pays the ticket's fees in the same call. - Receive.
ArbParentRootSourceon Robinhood accepts a block hash only from that sender's L1→L2 alias and only for Sepolia's domain, append-only: a different hash for the same block reverts. - Prove and render, as on Sepolia. The cache on Robinhood is the same contract code as Milestone 1, with Sepolia as its home. It verifies Sepolia's record and balance proofs against the delivered hash.
The host facet. Anything running on Robinhood already trusts Robinhood, including whoever can upgrade it, so
the grade is A relative to Robinhood, and the render must never show that letter bare. The resolver carries an
immutable, dated facet describing Robinhood's upgrade setup, read from the chain before deployment: every
upgrade path (the rollup contracts on Sepolia and the chain owner on Robinhood) ends at one Safe with a threshold
of 1 of 2 — one key, no delay. The Safe has no modules or guard on either chain, so its two signers are the only
ones who can act. So the line says instant upgrades, 1-of-2 Safe, as of 2026-10. The resolver
refuses to deploy without a facet when its cache's home is another chain, and its scope disclosure adds that the
facet was recorded at deployment and may have changed since.
What the delivered root does not establish (published on-chain by ArbParentRootSource): anything beyond the
host's consensus; that the Inbox and Bridge behave as published (they are upgradeable — see the facet); that a root
arrives at any particular time; that a delivery cannot be censored (the recourse is the rollup's forced inclusion,
after 4 days on this testnet); and anything the Sepolia same-chain source does not establish.
Freshness. Arbitrum includes a parent-chain message after the parent finalizes it, by default. Measured: a root
was readable on Robinhood 762 s after it was sent, and the first render was 781 s old. Use
maxAgeSeconds of at least 3600 on Robinhood.
Tokens ChainRender didn't create (snapshot mode)
The proofs above read a token's balance from its storage, which works because a ChainRender token's storage layout is fixed and recorded by its factory. An existing token — WETH, USDC behind its upgradeable proxy, a rebasing token — has no such record, and nothing on-chain can check a claimed storage layout against a contract's code. So instead of reading its storage, Milestone 3 runs its code:
- Snapshot on Sepolia (anyone).
BalanceSnapshottercalls the token's ownbalanceOf(holder)anddecimals()in a transaction, and stores a commitment to the answers — with the token's code hash, the block number and the block timestamp — in its own storage, whose layout is known.recordMetadata(token)does the same once forname()andsymbol(). Ownerless, permissionless, no fee. - Deliver a root to the other chain, exactly as for Milestone 2.
- Prove and render.
SnapshotBalanceCacheproves the snapshotter's commitment slots against the delivered root (the snapshotter's code hash is pinned), andSnapshotBalanceResolver(kindxchain-snapshot-balance) renders it.
The token's code does the interpreting, under Sepolia's consensus, so the answer is a same-chain fact of the same
kind ChainRender's view reads are: Grade A, relative to the host, with the host facet as before. Proxies,
packed balance words and rebasing need no special handling — whatever balanceOf returned is what is shown.
What exactly it states. "During a transaction in block N, this token's code (code hash H) answered
balanceOf(holder) with X." That is a mid-transaction observation, which is why the render says
(in-transaction). It is meant for display: an app that gates access or value on a balance should use the
end-of-block proofs above, or require two snapshots in different blocks that agree.
| Situation | Render |
|---|---|
| a fresh snapshot | <amount> <SYMBOL> <0x full address> · balanceOf @ block N (in-transaction) · <age>s old · Grade <G> (on <host>[ · <facet>]) |
| none fresh | stale (<SYMBOL> <0x full address> · balanceOf @ block N · Grade <G>[ · on <host> · <facet>]) |
| no metadata proven | unavailable (no metadata) |
| metadata only under different token code | unavailable (code changed) |
| no entry, bad arguments, out-of-bounds values | unavailable |
The full token address is always shown: anyone can deploy a token that calls itself "USDC". Decimals come from the snapshot itself. Name and symbol are used only if they were recorded under the same code hash as the snapshot and no later than it, so a token replaced in place cannot borrow its predecessor's identity. The grade is the lowest of the snapshot's source, the metadata's source, and A.
What it does not establish (published on-chain by the three contracts): the balance now; that the holder held it
outside that call (mid-transaction state can be flash-borrowed); that balanceOf is honest (it is the token's own
answer); that the token is what its symbol suggests; that its code is unchanged since (a proxy's code hash does not
change on upgrade); that the issuer cannot change balances; and that the snapshotter's fixed call gas was enough.
The snapshotter gives the token exactly CALL_GAS (100,000 here) and refuses to run with less, so a caller cannot
starve it into a fallback answer; but a token that needs more and silently catches its own failure would answer
wrong. Each token is therefore checked before use (scripts/xchain/m3-gates.mjs): its answers given CALL_GAS must
equal its unbounded answers. Sepolia WETH9 and USDC pass; whole-transaction gas estimates for their calls were 17,670–25,923, far below the cap.
Freshness costs a Sepolia transaction. A storage proof can read any later block for free; a snapshot is only as
new as the last time someone paid for one. Past maxAgeSeconds the render says stale (…).
import { buildMetadataProof, buildSnapshotProof, encodeSnapshotBalanceArgs,
KIND_XCHAIN_SNAPSHOT_BALANCE } from "./chainc/xchain-snapshot.js";
await snapshotter.write.recordMetadata([token]); // Sepolia, once per token (and after any code change)
await snapshotter.write.snapshot([token, [holder]]); // Sepolia, each time a fresh value is wanted
// … deliver a root for block n (as in Milestone 2), then on the host:
const m = await buildMetadataProof(sepoliaClient, snapshotter.address, token, n);
await cache.write.registerToken([source, n, token, m.name, m.symbol, m.codeHash, m.metaBlock, m.proof]);
const s = await buildSnapshotProof(sepoliaClient, snapshotter.address, token, holder, n);
await cache.write.refresh([source, n, token, holder, s.value, s.decimals, s.codeHash, s.snapBlock, s.snapTime, s.proof]);
const line = await resolver.read.resolve([KIND_XCHAIN_SNAPSHOT_BALANCE, encodeSnapshotBalanceArgs(token, holder, 7200n, 2)]);
The builders take each commitment's preimage from the snapshotter's events and check it against the proven slot
before anything is submitted. scripts/xchain/m3-snapshot.ts runs the whole sequence, and checks every on-chain
render against the chainc mirror renderSnapshotBalance at the same block. Accepted on 2026-10-06 with Sepolia
WETH9 and USDC: the lines above, with the root on Robinhood after 773 s.
Tokens ChainRender didn't create, without a transaction per value (storage-layout mode)
Snapshot mode needs a Sepolia transaction for every fresh value. Storage-layout mode reads an existing token's storage directly, at any block a root source vouches for — but nothing on-chain can check that a slot really is where the token keeps its balances, so it is honest only at Grade C:
0.03 WETH 0xfff9976782d46cc05630d1f6ebab18b2324d6b14 · proven @ block 11862735 · 488s old · Grade C (adopted · claim 0xfae8aac6…5d6cc4e5 · on Robinhood Testnet · instant upgrades, 1-of-2 Safe, as of 2026-10)
How a token is adopted. Anyone files a claim with TokenAdoptionRegistry on Sepolia. A claim names two small
adapters — a pinning rule ("which code is this token really running?") and a balance layout ("where is the
balance, and how is it encoded?") — by address and code hash, plus the token's code hash, the keccak256 of a
published evidence bundle, and a witness holder. At adoption the registry:
- checks the adapters are pure — it scans their code and refuses any reachable opcode outside a short allowed list (no storage, calls, logs, block or transaction data), so an adapter can never change its answer over time;
- runs structural checks on the token through the pinning rule — for
plain/v1: no reachableDELEGATECALLorCALLCODEand abalanceOfselector; forslot-proxy/v1: the proxy reads one constant implementation slot and the implementation does not delegate further; - reads the token's name, symbol and decimals and the witness's
balanceOf, under the same gas floor and return-size bounds as snapshot mode.
In a later transaction anyone confirms the claim: a same-chain proof of the adoption block must show the claimed slot holding exactly the witness's balance. Only confirmed claims can be registered by a cache on any chain.
Claims are content-addressed. The claim id is the hash of the whole record, so there is no "official" record per token: two people can adopt the same token under different claims, and a wrong claim only ever affects itself. An app binds a specific claim id, which the render prints (8+8 hex) next to the full token address.
What the code checks and what it does not. The chain checks code hashes, structure and one witness. It does not
check that the claimed layout is the token's real one — that rests on the evidence bundle: the token's verified
source recompiled to its exact on-chain bytecode, the balance slot derived by the compiler, and at least three holders
whose slot values equal their balanceOf at one block. chainc/adoption-evidence.ts reruns all of it; a client that
does may show Grade B for that claim. On-chain, every adopted read stays C, whatever an adapter declares.
Upgrades stop proofs, never mislead them. For a proxy, the claim pins the implementation slot's word and the
implementation's code hash; after an upgrade every later proof reverts (ImplementationChanged) and the app must bind
a new claim. Values proven before the upgrade still render as what they are — a past block, with its age.
Refused rather than mis-pinned. Sepolia USDC is refused: its implementation delegate-calls a linked library
(Delegates at instruction 18607). An on-chain scan cannot tell where a DELEGATECALL target comes from, so the rule
refuses every reachable one rather than risk accepting a hidden upgrade path. USDC is shown through snapshot mode
at Grade A instead.
Adding a kind of token = adding an adapter. The registry, cache and resolver never interpret a layout; they only run the adapters a claim names. A new layout or proxy kind is one new adapter with its own review — the test suite proves it with a fourth, test-only adapter that works end to end with no change to the core.
import { buildAdoptedStateProof, buildRegistryProof, encodeAdoptedBalanceArgs,
KIND_XCHAIN_ADOPTED_BALANCE } from "./chainc/xchain-adopted.js";
// home: adopt (claim id + record from the Adopted event), then confirm the witness at the adoption block
const { proof: w } = await buildAdoptedStateProof(sepoliaClient, record.claim, record.claim.witness, record.adoptedAt);
await registry.write.confirmWitness([record, w]);
// any chain with a root for block n: register the confirmed claim once, then refresh holders
const { proof: rp } = await buildRegistryProof(sepoliaClient, registry.address, claimId, n);
await cache.write.register([source, n, record, rp]);
const { proof } = await buildAdoptedStateProof(sepoliaClient, record.claim, holder, n);
await cache.write.refresh([source, n, claimId, holder, proof]);
const line = await resolver.read.resolve([KIND_XCHAIN_ADOPTED_BALANCE, encodeAdoptedBalanceArgs(claimId, holder, 7200n, 2)]);
The proof builders ask the claim's own adapters (pure eth_calls) which accounts and slots to prove, exactly as the
contracts do, so a new adapter needs no new client code. scripts/xchain/m3b-adopt-and-prove.ts runs the whole
sequence and checks each render against the chainc mirror. Accepted on 2026-10-07 with Sepolia WETH9 — its verified
source recompiles byte for byte; balance slot 3 — rendered on Sepolia and, 469 s after delivery, on Robinhood.
Which mode to use. Snapshot mode: Grade A, any ERC-20 including USDC, one Sepolia transaction per fresh value. Storage-layout mode: Grade C, no transaction per value, any later block, only for tokens whose code passes the structural checks.
Cost
Measured on Sepolia: a refresh with a real 9-node account proof costs 561,811 gas the first time for a holder
(creating its storage) and 510,415 gas when updating it, the latter measured after Glamsterdam (2026-10-06), which
prices newly created storage higher; registering a token is a
one-time cost, measured at roughly 430,000–480,000 gas in local tests (where proofs are shallower than on
Sepolia, so expect more there). Rendering is free. Proof depth grows with the size of the chain's
state, adding roughly 30,000 gas per extra proof node.
On Robinhood (measured 2026-10-06): delivering a root cost 219,748 gas on Sepolia plus about 0.0000122 ETH of prepaid ticket fees (estimates × 3; the unused part is refunded on Robinhood). The delivered root's execution on Robinhood cost 69,051 gas. Registering CRPD reported 881,376 gas and a refresh 813,008 gas, but an Arbitrum receipt includes the parent-chain data fee for the calldata (about 28–30 % here). The execution part of the refresh, 567,862 gas, is within about 1 % of the same first refresh on Sepolia (561,811). At Robinhood's 0.01 gwei, a register plus a refresh cost about 0.000017 ETH.
Snapshot mode (measured 2026-10-06, first time for each token and holder): on Sepolia, recordMetadata cost
151,815 (WETH9) and 155,432 gas (USDC), and snapshot for one holder 141,610 and 149,825 gas. On
Robinhood the execution was 589,189 / 586,969 gas to register the metadata and 543,070 / 543,058 to refresh
— below Milestone 2's 633,099 and 567,862, because the proof ends in the snapshotter's small storage (2 storage-proof
nodes) rather than a token's.
Storage-layout mode (measured 2026-10-07, WETH9, first time for each piece of state): on Sepolia adopt
1,393,910 gas (copies and scans the token's code; scans both adapters the first time they are used) and
confirmWitness 650,042; registering the claim in a cache 2,485,171 on Sepolia and 907,728 on Robinhood
(the full adoption record is stored); a first refresh 881,362 on Sepolia and 711,951 on Robinhood. WETH9's
storage trie is deep (6 storage-proof nodes against the snapshotter's 2), which is why a refresh costs more than in
snapshot mode. New state cost about 2.7× more on post-Glamsterdam Sepolia than on Robinhood in the same run.
Once set up, a fresh value costs less (repeat refreshes, measured 2026-10-07, all entries already existing):
| Sepolia | Robinhood | |
|---|---|---|
| Snapshot mode | snapshot 43,690 (WETH9) / 51,905 (USDC) |
refresh 486,077 / 486,053 |
| Storage-layout mode | refresh 675,618 (same-chain) |
refresh 674,987 |
Plus, for Robinhood, one root delivery per block: 219,712 Sepolia gas and a prepaid ticket. One delivery serves every refresh at that block, for any number of tokens, holders and both modes. The two chains charged almost exactly the same for the same repeat refresh, so the earlier gap came from creating new state, not from verifying proofs.
Security review
The proof stack went through two independent review passes before deployment. Every finding was fixed and has a
test, including: per-source entries so a relay can't lock a value, time-based staleness so a silent relay can't keep
its values fresh, display bounds on symbols and decimals, entries that stop rendering when the record they were
proven under is superseded (tested against a fully forged relay state), strict trie rules, and a scope-disclosure
reader that a hostile contract can't make revert. The details are in the implementation spec
(docs/176-xchain-milestone1-implementation-spec.md, §10.4). Milestone 2's spec, gate results and acceptance run are in
docs/177-xchain-milestone2-robinhood-spec.md (§10–§12); Milestone 3's design reviews, as-built notes and acceptance
run are in docs/178-xchain-milestone3-adopted-tokens-spec.md (§12–§16).
Swapping across chains without a bridge (Milestone 4)
Proven balances never move value. Milestone 4 adds the one way to exchange value across Sepolia and Robinhood that needs no bridge and no escrow anyone else controls: a hash-timelock swap. Alice locks a Sepolia token under the sha256 hash of a secret; Bob locks a Robinhood token under the same hash; revealing the secret on Robinhood pays Alice, and the same secret then pays Bob on Sepolia. If nobody claims, each lock refunds its owner after its own deadline. The token on each chain never leaves that chain; only its owner changes.
What ChainRender adds is that the second lock checks the first one by storage proof. Bob's Robinhood lock names
SwapLockProofCache as its guard and opens only if Robinhood has proven that Alice's Sepolia lock is open, locks a
proven ChainRender token, uses the same hash, pays Bob, and outlasts Bob's lock by a safety gap. The gap and Bob's
minimum lock length are derived from Robinhood's own time bounds (its SequencerInbox's maxTimeVariation), so Alice
can still claim through Sepolia's delayed inbox if Robinhood's sequencer ignores her. A swap-lock resolver renders
the proven lock:
Sepolia lock open · 25.00 CRSA · to 0x256f87f6…59c180ac · ends in 305h 40m · proven @ block 11864092 · 612s old · Grade A (on Robinhood Testnet · instant upgrades, 1-of-2 Safe, as of 2026-10)
The proven state is Grade A relative to Robinhood, with its facet, like every proven read there. The swap as a whole
is Grade C at most: it needs each claim included before its deadline, and Robinhood's operators can delay
transactions. The same HashTimelockSwap contract, at the same address with the same code, runs on both chains.
The showcase page (/chainrender-crosschain.html on the site) walks the whole use case with the visitor's own wallet:
create a token on either chain (name, symbol, decimals, supply), send on each chain, prove a balance across chains,
swap, follow every event per chain, and compare the trust model with Robinhood's canonical bridge, tacit and EEZ.
Status: live on Sepolia and Robinhood Chain Testnet since 2026-10-07, accepted end to end (addresses in
Deployed Contracts). In the acceptance run a Sepolia root took 591 s to reach
Robinhood; proving the lock there cost 651,525 gas, the guarded second lock 159,466, and each claim 71,967 (Robinhood)
and 168,495 (Sepolia). On this testnet Robinhood allows a 4-day delay window, so the second lock must run at least
8.5 days and the first about 4.1 days longer: a swap where both sides claim settles in minutes, but a refund waits
that long. Design, timing rules, review and the as-run record: docs/179-xchain-milestone4-transfers-spec.md.
What's next
- More adapters. Beacon proxies, minimal (EIP-1167) proxies, other packings and multi-slot layouts (vault shares, rebasing tokens) — each one new adapter, no change to the registry, caches or resolver.
- Proven view calls without a transaction. A zero-knowledge proof of a
balanceOfcall against a block's state would give snapshot mode's Grade-A answer without a Sepolia transaction per value; it plugs into the same seams. - Spending stays home. Proven balances are display and gating only. Value changes hands across chains only by the swap above, where each token stays on its own chain. A spendable copy of a token on another chain would need a bridge, and on an instantly upgradeable host that is Grade C at most (the upgrade-key rule).
- Proofs in the other direction. Robinhood-home tokens can't yet be proven on Sepolia; that needs a root source reading Robinhood's confirmed state from its rollup contract on Sepolia.
Related
- The Trust Grade — the A / B / C model these grades follow
- Resolvers — how a data slot binds a resolver kind
- DeFi Capabilities — per-chain DeFi reads
- Deployed Contracts — the live addresses