# 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](../contracts/deployed-contracts.md), *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:

1. **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.
2. **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.
3. **Render (free).** The `xchain-balance` resolver is a plain `view`. 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](./trust-grade.md), 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.

```ts
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:

1. **Deliver (anyone, paid by whoever wants a fresh root).** `ArbRootSender` on 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.
2. **Receive.** `ArbParentRootSource` on 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.
3. **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; across Milestones 2–4
deliveries took **469–773 s**. Use `maxAgeSeconds` of at least 3600 on Robinhood.

**Nobody delivers on a schedule.** Delivery is on demand — there is no keeper or operator — so between deliveries a
proven value simply ages; proven values have been seen **10.6–10.8 hours** old (doc 179 §14.8). The render says so
(`stale (…)` past your `maxAgeSeconds`); a paid or scheduled delivery is planned, not built (doc 181 §3.3). A relay
root source (`RelayRootSource`, Grade C) is built but **not deployed**.

## 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**:

1. **Snapshot on Sepolia (anyone).** `BalanceSnapshotter` calls the token's own `balanceOf(holder)` and `decimals()`
   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 for
   `name()` and `symbol()`. Ownerless, permissionless, no fee.
2. **Deliver a root** to the other chain, exactly as for Milestone 2.
3. **Prove and render.** `SnapshotBalanceCache` proves the snapshotter's commitment slots against the delivered root
   (the snapshotter's code hash is pinned), and `SnapshotBalanceResolver` (kind `xchain-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 (…)`.

```ts
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 reachable `DELEGATECALL` or
  `CALLCODE` and a `balanceOf` selector; for `slot-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.

```ts
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_call`s) 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](../contracts/deployed-contracts.md)). 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 `balanceOf` call 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

- [All Resolver Kinds](../reference/resolver-kinds.md) — exact args and renders of `xchain-balance`,
  `xchain-snapshot-balance`, `xchain-adopted-balance` and `swap-lock`, generated from the code
- [The Trust Grade](./trust-grade.md) — the A / B / C model these grades follow
- [Resolvers](./resolvers.md) — how a data slot binds a resolver kind
- [DeFi Capabilities](./defi-capabilities.md) — per-chain DeFi reads
- [Deployed Contracts](../contracts/deployed-contracts.md) — the live addresses
