A Portaldot light client on Ethereum and EVM chains. Solidity contracts check Portaldot's GRANDPA finality (the validators' signatures) and Substrate storage proofs, so an EVM contract can use Portaldot state without a trusted relayer: trust rests on the Portaldot validator set.
The bridge app: jonathan-moore58.github.io/bridge-zero/app. Lock tPOT on the Portaldot V3 testnet and receive wtPOT on Sepolia, Base Sepolia, Arbitrum Sepolia or the BNB Smart Chain testnet, and burn it back, from MetaMask or a Polkadot wallet. Its Verify tab checks any transfer in your browser: V3's validator signatures, the lock's storage proof, and the destination contract's verdict.
The status page: jonathan-moore58.github.io/bridge-zero. It reads the light clients, the vault and the bridge from the chains as you watch, and its "Check it live" buttons send the tampered mints to BridgeMinter as read-only calls, which it refuses. It has no wallet and signs nothing.
Deploy it yourself: docs/DEPLOY.md. Build and test everything, run the tools against the live deployment as dry runs, deploy a copy of your own, and run the relayer, auto-mint and the release watcher, on a Linux server too (ops/server/).
Built for the Portaldot Hacker House 2026, Multi-Chain & Cross-Chain Infra track. Development runs from Oct 14 to Nov 6, 2026; this repository starts on Oct 14.
Three layers, each live on testnets; the rows are the evidence.
| Layer | What it is | Live |
|---|---|---|
| The app | The bridge app: lock tPOT on V3 and receive wtPOT on Sepolia, Base Sepolia, Arbitrum Sepolia or the BNB Smart Chain testnet, burn it back, and verify any transfer in the browser. The status page reads the light clients, the vault and the bridge from the chains. | Public since Oct 31; its first transfers, from the owner's own wallet with the app run locally, on Oct 28 (rows 114 to 126) |
| The proof kit and the data bridge | GrandpaLightClient on four EVM chains, which accepts a V3 header only with the V3 validators' signatures, checked in Solidity; the storage-proof libraries; BridgeMinter and wtPOT, which mint only a lock proven from the vault's storage; and the proof kit, through which any contract reads proven V3 balances, asset balances, NFT owners and contract slots. |
On Sepolia since Oct 17 (rows 1 to 8), its first proven mint on Oct 19 (row 32); on Base Sepolia and Arbitrum Sepolia since Oct 27 (rows 59 to 82); the kit on Sepolia since Oct 31 (rows 127 and 128); on the BNB Smart Chain testnet since Nov 1, both ways (rows 129 to 149) |
| Operations | The relayers that keep each light client at its chain's validator set, auto-mint, the release watcher of the 2-of-3 committee, the rollups' relay runs and a health check, as services on a small cloud server (ops/server/), and the deployment guide. | On the server since Oct 31, 17:45 UTC; before that on a laptop, with gaps while it slept or was off (Known limits) |
Proven, trusted, and the path to narrow the trust. Proven on chain: each V3 header's GRANDPA signatures (2 of 2 on V3; the same code checked 7 of 10 on the old mainnet), the storage proof of each lock and each read, and the commitment the minter recomputes. Anyone can relay or mint, and a relayer that stops can delay a mint, not forge one. Trusted today: V3's 2 validators (probably development keys) and its sudo key; each light client's admin, who set its checkpoint and can pause it and install a new one, so it is trusted like the validator set; the way back, where a 2-of-3 committee releases tPOT and nothing on V3 checks the burn (on the testnet one operator holds all three keys, two of them on a cloud server since Oct 31, and fast mode also trusts that a burn stays once 3 Sepolia blocks, or 36 seconds of a rollup's blocks, follow it); and the vendored ed25519, BLAKE2b and trie code, treated as unaudited. The path, from docs/BUSINESS.md, which leaves V3's validators and sudo key trusted:
| When | Step | What it changes |
|---|---|---|
| Nov 10 to Dec 9 | LP fast-fill on the way back: a liquidity provider pays the user at once, and the committee releases to it only once the burn is final; the threat model and the audit's preparation | a fast way back in which the vault pays only after finality; the provider carries the wait and the reorg risk |
| Dec 10 to Jan 9 | A BEEFY verifier prototype; a light client of Portaldot's V3 mainnet, from its own checkpoint | cheaper imports under the same validators; the same proofs on Portaldot's V3 mainnet |
| Jan 10 to Feb 9 | An outside audit; each light client's admin behind a timelock or a multisig, or renounced; committee keys held by independent members; a mainnet launch with caps on the total and on each transfer | the admin, the single operator, and code nobody has audited |
| After Feb 9, not scheduled yet | A proven way back: an Ethereum light client on Portaldot, so that the vault checks a burn's proof instead of the committee's approval | the committee |
Milestones.
- Done, Oct 14 to Nov 1 (development log): the light client verifying two Portaldot chains on Sepolia (Oct 17); the vault and the first proven mint (Oct 19); the trusted way back and lock to mint in 48 seconds (Oct 20); the relayer in watch mode (Oct 23); fast mode, back in 84 seconds (Oct 24); auto-mint, and the V3 light client on Base Sepolia and Arbitrum Sepolia (Oct 27); both ways on both rollups, and the first transfers from the owner's own wallet through the app (Oct 28); the Verify page, the app made public, the proof kit on Sepolia, the server, CI and the rollups' contracts source-verified (Oct 31); the BNB Smart Chain testnet as a fourth destination, both ways, back in 46 seconds at its finality (Nov 1).
- Nov 10 to Feb 9: the steps in the table, with a v2 vault and minter (a bridge fee, fast-fill for the way in), the proof kit as packages and Base as the first mainnet; the roadmap has each.
Portaldot's own bridge and iBridge. Portaldot runs its own bridge,
bridge.portaldot.dev, between its live V3 network ("PortaldotV3
Live") and BNB Smart Chain, where POT is the BEP-20 token WPOT: a user sends POT, or WPOT, to the
bridge's address on one chain from the Portaldot extension or MetaMask, then files the transfer
with the bridge's service, which pays out on the other chain (as its page and its public
configuration read on Oct 31). It serves the live networks; Bridge Zero runs on testnets, and
replaces neither it nor iBridge, the cross-chain bridge in Portaldot's whitepaper. Bridge Zero is a
building block that either could use: a light client with which a contract on an EVM chain checks
Portaldot's finality and storage itself, as BridgeMinter does for each lock. Since Nov 1 it also
reaches the BNB Smart Chain testnet, the test network of the chain Portaldot's bridge serves (rows
129 to 149).
GrandpaLightClient runs on Ethereum Sepolia, Base Sepolia, Arbitrum Sepolia and the BNB Smart
Chain testnet and follows the finality of a Portaldot chain: it accepts a block header only with valid signatures from more than
two thirds of the chain's GRANDPA validators, checked in Solidity, and then holds that block's state
root, against which contracts check Substrate storage proofs. The bridge is built on it: tPOT locked
in a vault on the Portaldot V3 testnet becomes wtPOT on the chain the lock names, Sepolia, Base
Sepolia, Arbitrum Sepolia or the BNB Smart Chain testnet, in one transaction that verifies V3's finality, proves the lock from
the vault's storage, and mints. No trusted relayer: anyone can bring the proofs, and the contracts
check them. What is trusted is the Portaldot validator set, each chain's sudo key and each light
client's admin (Trusted); the way back (burn wtPOT, release tPOT on V3) is trusted
too, a 2-of-3 committee, and ran on Oct 20 after Sepolia's finality (rows 47 to 51), on Oct 24 in
fast mode, 84 seconds from the burn's block to the release's (rows 56 to 58), and on Oct 28 from
Base Sepolia and Arbitrum Sepolia, after each rollup's finality (rows 108 to 113) and in fast mode
for the owner's own wallet (rows 121 to 126), and on Nov 1 from the BNB Smart Chain testnet, 46
seconds from the burn's block to the release's, after that chain's finality (rows 147 to 149).
So Bridge Zero is an asset bridge and a data bridge. The asset bridge moves tPOT to Sepolia, Base Sepolia, Arbitrum Sepolia and the BNB Smart Chain testnet as wtPOT and back. The data bridge is the light client with the proof libraries: any EVM contract can use them to read proven Portaldot state, as ProvenBalance (an account's balance) and BridgeMinter (a lock in the vault's storage) do, and they are a building block for iBridge. The proof kit gathers them for other builders: balances, asset balances, NFT owners and contract slots of V3, read through one Solidity library or one stateless contract, with the tools that fetch and check the proofs. Today the bridge connects the Portaldot V3 testnet with Ethereum Sepolia, two rollups that settle on it, Base Sepolia and Arbitrum Sepolia, and the BNB Smart Chain testnet: one vault on V3 serves all four. The roadmap plans a liquidity engine for transfers that settle in seconds and the same contracts on more EVM chains that have the BLAKE2F precompile.
| Where | What it proves | |
|---|---|---|
| Lock 0 minted (row 32) | Sepolia transaction 0x91714c84… |
One transaction verified V3 block #108,424 with both V3 validators' signatures, bound the vault, proved lock 0's commitment from the vault's storage and minted 1 wtPOT. Its Logs tab lists HeaderImported, VaultBound, Transfer and Minted. |
| Lock to mint in 48 seconds (rows 54 and 55) | Sepolia transaction 0xb9134dd7…; the lock in V3 block #118,636 |
Lock 1, Oct 20: 1 tPOT locked on V3 and minted on Sepolia, the mint's block 36 seconds after the lock's by the block timestamps; 48 seconds from sending the lock to the mint's receipt with the tools' own checks. |
| Auto-mint: the user signs only the lock (rows 83 to 85) | Sepolia transaction 0xb6164465…; the lock in V3 block #219,589 |
Lock 2, Oct 27: 0.01 tPOT locked on V3. The operator's auto-mint saw the lock, waited for V3's finality and a 90-second grace, and sent the mint on Sepolia from a key of its own, which paid the gas: the mint's block 144 seconds after the lock's. The user sent nothing for the mint, and the minter paid only the address the lock commits to. |
| Auto-mint for a lock from a Portaldot account (rows 86 and 87) | Sepolia transaction 0xb6e2a07b…; the lock in V3 block #222,024 |
Lock 3, Oct 27: 0.001 tPOT locked by a revive.call signed by a 5… account, the way the app's Portaldot Wallet path sends a lock. Its Locked event is only in V3's System.Events, not in eth_getLogs; auto-mint found it there and minted it on Sepolia from its own key. |
BridgeMinter |
Sepolia 0x8287…cB9E |
The minter, source-verified, with no owner and no admin. Rows 32 to 38, 47, 55, 56, 85 and 87 are sent to it: lock 0's mint, then five tampered copies, each reverted with the error that names what was changed, a replay, reverted with NonceAlreadyUsed(0), the mints of locks 1, 2 and 3, and burns 0 and 1, the trusted way back. |
| The V3 light client | Sepolia 0x4471…dcb0 (GrandpaLightClient #2) |
Follows the V3 testnet's finality from a checkpoint at validator set 19: every set change since, imported in order, each with both validators' signatures; currentSetId() read 75 at 20:42 UTC on Oct 31. Rows 5, 6, 18 to 23, 26 to 31, 44 to 46 and 52 take it to set 33; the relayer's later runs, to Oct 28, are in deployments/relay-sepolia-0xd983….jsonl and, since Oct 23, deployments/relay-sepolia-0xe94a….jsonl. It is as current as the relayer's last run (see Known limits). |
| The proof kit (rows 127 and 128) | Sepolia 0x2ea6…1fA9 (PortaldotStateReader) and 0x24aB…8d23 (ProvenAssetGate) |
Oct 31: a stateless contract through which any contract reads V3 state that the light client finalized, at a block it imported: an account's balance, an asset balance, an NFT's owner, a contract's code hash and its slots. Source-verified, with no owner and no admin. Asked by eth_call, it returned the deployer's V3 account at #264,729 (159.47 tPOT free) for 452,621 gas, and the vault's commitments[1], lock 1's commitment, for 877,450. The example gate records holders of a V3 asset on top of it (For builders). |
| The V3 light client and the bridge on Base Sepolia and Arbitrum Sepolia (rows 59 to 82 and 88 to 126) | Base Sepolia 0x3897…0ab6 and Arbitrum Sepolia 0x3897…0ab6 (GrandpaLightClient #2 on each) |
Oct 27: the same contracts, from the same build as on Sepolia, on two rollups that settle on Sepolia. On each, the client verified V3 block #214,510 with both V3 validators' signatures and proved an account balance from its state root (rows 68 and 76), then the relayer imported V3's validator-set change 59 → 60 and a newer head (rows 79 to 82). Later the same day the relayer brought both clients to V3's set 62, and BridgeMinter and wtPOT were deployed on both, at 0xdc31…2348 and 0xE113…A95e, bound to the same V3 vault (rows 88 to 97); on Oct 28 one auto-mint process minted lock 4 on Base Sepolia and lock 5 on Arbitrum Sepolia, each first mint binding the vault (rows 98 to 107, the way in): one V3 vault now backs wtPOT on three chains. The same day the release tool and its watcher took the rollups' burns too: 0.001 wtPOT was burned on each (rows 108 and 109), and the watcher released both on V3, each burn final on its rollup (rows 110 to 113, the way back). Later that day the owner's own wallet used both routes through the app (rows 114 to 126, the next row). All eight contracts on each rollup, the four libraries, the client, ProvenBalance #2, BridgeMinter and wtPOT, are source-verified on Basescan and Arbiscan. On Sepolia, 0x3897…0ab6 is the old-mainnet client. |
| The app: the owner's own wallet, both ways on both rollups (rows 114 to 126) | Base Sepolia mint 0x7d9e535d… and Arbitrum Sepolia mint 0xb351c94b…; the releases in V3 blocks #231,461 (Base Sepolia's burn) and #231,273 (Arbitrum Sepolia's) |
Oct 28, from the app run locally: the owner's MetaMask wallet locked 1 tPOT on V3 for each rollup, and auto-mint minted each, the mint's block 139 and 133 seconds after the lock's. The wallet burned both, and the watcher released them on V3 in fast mode: 105 seconds from Arbitrum Sepolia's burn block to the release's, and 905 seconds from Base Sepolia's, which waited for the shared fast budget (the app's first transfers). |
| The BNB Smart Chain testnet, both ways (rows 129 to 149) | BNB Smart Chain testnet mint 0x81f73eb9… and burn 0x0a904a75…; the release in V3 block #275,615 |
Nov 1: the same contracts at the same addresses on a chain that is not a rollup of Sepolia and finalizes its blocks in about a second. Auto-mint minted lock 8 there, binding the vault, the mint's block 125 seconds after the lock's; the deployer burned it back, and the watcher released it on V3 once the burn was final there, 46 seconds from the burn's block to the release's, with no fast mode (the BNB Smart Chain testnet). The app offers it as a fourth destination. All eight contracts there are source-verified on BscScan. |
| The vault and lock 0 | V3 testnet: vault 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd, lock transaction 0x2516e421d29e5bcb0032253dc66db87acc3cc5b22b021252fcb7c2cfdad56051 (block #101,009) |
1 tPOT locked for Sepolia; the vault stores the commitment 0x9eba08c9…41e23435 that the mint proved. The official V3 testnet explorer shows the deployment and lock 0's event; the Portaldot Node Console shows the deployment block and the lock's block, and cast reads the vault. |
| The way back (rows 47 to 51, trusted) | Sepolia burn 0x42d70ae9…; the release in V3 block #117,701 |
0.5 wtPOT burned on Sepolia for 0xD983…1071 on V3; 2 of the 3 committee members approved the release through pallet-multisig, and the vault paid that address 0.5 tPOT once, for that burn id. No contract checks the release against the burn: the committee is trusted, and on the testnet one operator holds all three of its keys. |
| The way back in 84 seconds (rows 56 to 58, trusted, fast mode) | Sepolia burn 0x4e5f4c6e…; the release in V3 block #173,062 |
Burn 1, Oct 24: 0.1 wtPOT burned on Sepolia for 0xD983…1071 on V3, and released to it by the auto-release watcher with 3 confirmations instead of Sepolia's finality; the release's block 84 seconds after the burn's by the block timestamps. The same trusted committee, for burns of up to 1 tPOT; a reorg deeper than 3 blocks could undo a burn after its release. |
| Old mainnet, 7 of 10 (row 3) | Sepolia transaction 0x8ddd807e… |
The same light client code verified old-mainnet block #2,726,202 with 7 of its 10 validators' signatures and proved an account balance from that block's state root (Logs: HeaderImported with signatures 7, then BalanceProven). The old mainnet's explorer shows the same block hash and state root: #2,726,202. |
The evidence tables number 149 transactions: 58 on Sepolia, 25 on V3 (locks 1 to 8, the trusted return path's 16 and the transfer that funded the owner's wallet), 24 on Base Sepolia, 24 on Arbitrum Sepolia and 18 on the BNB Smart Chain testnet.
flowchart TB
subgraph V3["Portaldot V3 testnet"]
VAL["GRANDPA validators<br/>2 keys, both sign"]
VAULT["PortaldotVault 0xa02E…8ffd<br/>lock() stores commitments[nonce]"]
end
subgraph OFF["Off-chain tools, relayer/"]
RELAY["Relayer<br/>npm run relay"]
TOOL["Mint tool<br/>npm run mint"]
end
subgraph SEP["Ethereum Sepolia"]
LC["GrandpaLightClient #2 0x4471…dcb0<br/>finalized V3 headers and state roots"]
BM["BridgeMinter 0x8287…cB9E<br/>proves a lock, mints"]
WT["WrappedTPOT 0x1fcB…Cb14<br/>wtPOT"]
LIB["Linked libraries: SCL_EIP6565,<br/>GrandpaCodec, AuthoritySets"]
SP["Linked library:<br/>StorageProofs"]
end
subgraph RET["Trusted return path (rows 47 to 51 and 56 to 58)"]
BURN["Burn on Sepolia<br/>BridgeMinter.burn emits BurnRequested"]
COMM["2-of-3 committee<br/>pallet-multisig account on V3"]
REL["Release on V3<br/>PortaldotVault.release pays tPOT"]
end
VAL -->|"finality proofs"| RELAY
VAL -->|"finality proof of block B"| TOOL
VAULT -->|"storage proofs at B"| TOOL
RELAY -->|"set changes and heads"| LC
TOOL -->|"importAndMint: one transaction"| BM
BM -->|"import B, read its state root"| LC
BM -->|"mint"| WT
LC --- LIB
BM --- SP
WT -.->|"a holder burns wtPOT"| BURN
BURN -.->|"TRUSTED"| COMM
COMM -.->|"TRUSTED: 2 of 3 approve"| REL
classDef trusted stroke-dasharray: 6 4
class BURN,COMM,REL trusted
sequenceDiagram
actor U as Locker
participant V as Portaldot V3 testnet<br/>vault, node, validators
participant T as Mint tool<br/>(off-chain)
participant M as BridgeMinter<br/>(Sepolia)
participant L as GrandpaLightClient<br/>(V3 client, Sepolia)
participant W as WrappedTPOT<br/>(Sepolia)
U->>V: vault.lock(11155111, minter, recipient)<br/>with 1 tPOT
V->>V: commitments[nonce] = keccak256(tag,<br/>420420777, vault, 11155111, minter,<br/>recipient, amount, nonce)
V->>V: GRANDPA finalizes the block<br/>with the lock, then later blocks
T->>V: at the latest finalized block B:<br/>header, grandpa_proveFinality(B)
T->>V: read proofs at B: the vault's<br/>account entry, its child-trie root,<br/>commitments[nonce]
Note over T: checks every signature and proof,<br/>dry-runs the call on a Sepolia node
Note over L: the relayer keeps it at<br/>V3's current validator set
rect rgba(128, 128, 128, 0.12)
Note over T,W: one Sepolia transaction
T->>M: importAndMint(header,<br/>finalityProof, signers, lockProof)
M->>L: importFinalityProof(header,<br/>finalityProof, signers)
L->>L: hash the header (blake2b-256):<br/>it must be B, the block<br/>the proof finalizes
L->>L: verify 2 of 2 ed25519<br/>precommit signatures against<br/>the set it follows
L->>L: store B's state root<br/>(HeaderImported)
L-->>M: B (the lock proof must name B)
M->>L: stateRootOf(B), read for the bind<br/>and again for the mint
opt vault not bound yet (first mint)
M->>M: prove the vault's account entry:<br/>code hash, trie id (VaultBound)
end
M->>M: prove the child-trie root,<br/>then commitments[nonce]
M->>M: recompute the commitment:<br/>it must equal the proven word
M->>M: used[nonce] must be false,<br/>then set it
M->>W: mint(recipient, amount)
Note over M,W: Transfer, Minted
end
No trusted relayer. The relayer and the mint tool carry data; the Sepolia contracts check all of it. The light client builds the signed GRANDPA message on chain, reads the validator keys from its own storage, and imports a header only with valid signatures from N - floor((N - 1) / 3) of the N validators it follows (2 of 2 on V3, 7 of 10 on the old mainnet). The minter reads the vault only through storage proofs against an imported state root, and mints only the lock the proven commitment describes. Anyone can run the relayer or send a mint; a relayer that stops can delay a mint, not forge one.
Trusted: the Portaldot validator set (on V3: 2 validators, probably development keys) and each
chain's sudo key, which can change any storage (V3 and the old mainnet both have one, Sudo.Key in
fixtures/probe/); each light client's admin, the deployer, who set its
checkpoint, can pause it at any time and install a new one while it is paused, so a checkpoint it
installs counts like a finalized block and the admin is trusted like the validator set (in
production it would be a timelock or a multisig, or renounced); and the return path, where 2 of
the 3 committee members release tPOT through pallet-multisig and nothing on V3 checks the burn. docs/BRIDGE.md lists every assumption.
These read the chains and need nothing but Foundry's cast; none of them
signs or sends anything. They are written for bash or zsh, and the outputs are what cast 1.8.5
printed on Oct 30, 2026 (UTC), after the releases of rows 121 to 126. The two marked as growing grow
with each relayer run, and the wtPOT and way-back outputs change with any later mint, burn or
release. Any Sepolia RPC works in place of the public one.
SEPOLIA=https://ethereum-sepolia-rpc.publicnode.com
V3=https://test-evm-rpc-v3.portaldot.dev
# GrandpaLightClient #2 (Sepolia): the V3 validator set it is at, and the latest V3 block it holds.
# Both grow with each relayer run; the mint (row 32) verified block #108,424, in set 30.
cast call 0x447171c4885Ad725eE4772E8e3c228aDeE50Dcb0 "currentSetId()(uint64)" --rpc-url $SEPOLIA
# 30 or higher (grows with each relayer run)
cast call 0x447171c4885Ad725eE4772E8e3c228aDeE50Dcb0 "latestFinalized()(bytes32,uint32)" --rpc-url $SEPOLIA
# the hash of the latest V3 block it holds
# its number: 108424 or higher (grows with each relayer run)
# Signatures each client needs: 2 of the V3 testnet's 2, 7 of the old mainnet's 10
cast call 0x447171c4885Ad725eE4772E8e3c228aDeE50Dcb0 "threshold()(uint256)" --rpc-url $SEPOLIA
# 2
cast call 0x389781d4856f0a9f3266ca6e76be6c7b7cfe0ab6 "threshold()(uint256)" --rpc-url $SEPOLIA
# 7
# The state roots the clients hold for V3 block #108,424 (row 32; the root in its header on the V3
# node) and for old-mainnet block #2,726,202 (row 3; the root the old mainnet's explorer shows)
cast call 0x447171c4885Ad725eE4772E8e3c228aDeE50Dcb0 "stateRootOf(bytes32)(bytes32,uint32)" 0xc89b7e6c5d7596897e677d464a9cc5ca373a69412f810d3f003d7aa8761e4e6d --rpc-url $SEPOLIA
# 0x0323c7bf619aefcc5360ef3977f8761a57c3a2fe090346bc95be7323d0d9c264
# 108424 [1.084e5]
cast call 0x389781d4856f0a9f3266ca6e76be6c7b7cfe0ab6 "stateRootOf(bytes32)(bytes32,uint32)" 0xae013833a74184522e47e70e41c895f29dec7e59d25504e1fb89b8eb87f3c14f --rpc-url $SEPOLIA
# 0xf8cd5d2b26dc43dd577ba4fde39fbc99d61835a0c1984381058f032f44be0e34
# 2726202 [2.726e6]
# BridgeMinter (Sepolia): lock 0 is minted, and the vault's trie id was bound from a proof
cast call 0x8287B3Baf205CA8456f98346F13064AA7fCDcB9E "used(uint256)(bool)" 0 --rpc-url $SEPOLIA
# true
cast call 0x8287B3Baf205CA8456f98346F13064AA7fCDcB9E "vaultTrieId()(bytes32)" --rpc-url $SEPOLIA
# 0xbe307c2d11dbc9391cda93d17e9772542d1300c9af14e7431918fb49e2f47c3a
# WrappedTPOT (Sepolia, 14 decimals): locks 0 and 1 minted 1 wtPOT each, and auto-mint minted lock
# 2's 0.01 (row 85) and lock 3's 0.001 (row 87), all to the same recipient, who burned 0.5 (row 47)
# and 0.1 (row 56); nothing else is minted
cast call 0x1fcBbd66922AC5AA20219a5c16c2591A76B4Cb14 "balanceOf(address)(uint256)" 0xD9838A86cF83C28Ee486544060D1cA4Ca2971071 --rpc-url $SEPOLIA
# 141100000000000 [1.411e14]
cast call 0x1fcBbd66922AC5AA20219a5c16c2591A76B4Cb14 "totalSupply()(uint256)" --rpc-url $SEPOLIA
# 141100000000000 [1.411e14]
# The vault (V3 testnet, through its EVM RPC): 899 bytes of code, whose keccak-256 is the code hash
# BridgeMinter was deployed with (its vaultCodeHash)
cast codesize 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd --rpc-url $V3
# 899
cast code 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd --rpc-url $V3 | cast keccak
# 0xa72553f50cdf645fb343b2cf089e431a1b26fb07e80f4e2236d55bf9811c663f
# Lock 0's commitment as the vault stores it on V3, and the same word recomputed by the minter on
# Sepolia from the lock's recipient, amount (10^14 native units, 1 tPOT) and nonce
cast call 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd "commitments(uint256)(bytes32)" 0 --rpc-url $V3
# 0x9eba08c90e4527bdb00da438eb8f15ad244cbb202e8886d929b0e12941e23435
cast call 0x8287B3Baf205CA8456f98346F13064AA7fCDcB9E "commitmentOf(address,uint256,uint256)(bytes32)" 0xD9838A86cF83C28Ee486544060D1cA4Ca2971071 100000000000000 0 --rpc-url $SEPOLIA
# 0x9eba08c90e4527bdb00da438eb8f15ad244cbb202e8886d929b0e12941e23435
# The lock itself: the vault's Locked event at V3 block #101,009 (abridged)
cast logs --from-block 101009 --to-block 101009 --address 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd --rpc-url $V3
# - address: 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd
# blockNumber: 101009
# data: 0x0000…00aa36a7 0000…8287b3baf205ca8456f98346f13064aa7fcdcb9e 0000…5af3107a4000 9eba08c90e4527bdb00da438eb8f15ad244cbb202e8886d929b0e12941e23435
# topics: [
# 0x56431a42ac501689c7bf63a9cab5bc9a191019a6321793149852e1bbe6db157c
# 0x0000000000000000000000000000000000000000000000000000000000000000
# 0x000000000000000000000000d9838a86cf83c28ee486544060d1ca4ca2971071
# 0x000000000000000000000000d9838a86cf83c28ee486544060d1ca4ca2971071
# ]
# transactionHash: 0x2516e421d29e5bcb0032253dc66db87acc3cc5b22b021252fcb7c2cfdad56051
# The way back (trusted, rows 47 to 51 and 56 to 58): burns 0 and 1 on Sepolia and their burn ids,
# then on V3 the vault's released flag for each id, and what it still holds for wtPOT on the three
# chains (4.013 tPOT locked, 2.602 released)
cast call 0x8287B3Baf205CA8456f98346F13064AA7fCDcB9E "burnCount()(uint256)" --rpc-url $SEPOLIA
# 2
cast call 0x8287B3Baf205CA8456f98346F13064AA7fCDcB9E "burnIdOf(uint256)(bytes32)" 0 --rpc-url $SEPOLIA
# 0x0b8e808d1084dd2878e3d20b124a677cdc26785882cf3aac9f7ccbf84245471f
cast call 0x8287B3Baf205CA8456f98346F13064AA7fCDcB9E "burnIdOf(uint256)(bytes32)" 1 --rpc-url $SEPOLIA
# 0x1573ff21c0f6cae223a42566b061f39bc0a982dafe451b86b29a8bf5f9219c0e
cast call 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd "released(bytes32)(bool)" 0x0b8e808d1084dd2878e3d20b124a677cdc26785882cf3aac9f7ccbf84245471f --rpc-url $V3
# true
cast call 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd "released(bytes32)(bool)" 0x1573ff21c0f6cae223a42566b061f39bc0a982dafe451b86b29a8bf5f9219c0e --rpc-url $V3
# true
cast call 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd "outstanding()(uint256)" --rpc-url $V3
# 141100000000000 [1.411e14]The Locked event's data is the destination chain (0xaa36a7, 11155111), the minter, the amount
(0x5af3107a4000, 10^14 native units) and the commitment; its topics are the event, nonce 0, the
sender and the recipient. The V3 EVM RPC returns the lock's log. For some days it did not serve
the lock's receipt; on Oct 23 cast receipt 0x2516e421d29e5bcb0032253dc66db87acc3cc5b22b021252fcb7c2cfdad56051 --rpc-url $V3
returned it again (status 1, to the vault, block 101009). For the Sepolia rows,
Etherscan has every receipt; the public RPC above returns none for the older rows. In a browser,
the Portaldot Node Console, a Polkadot.js Apps interface that reads the V3 node directly (it is
not an indexed explorer), shows block
#100,955
with the vault's revive.instantiateWithCode and revive.Instantiated, and block
#101,009
with lock 0's revive.ethTransact and revive.ContractEmitted. The release is a Substrate
extrinsic (multisig.asMulti dispatching revive.call), so the EVM RPC returns no Released log
for it; block
#117,701
shows multisig.MultisigExecuted and the vault's revive.ContractEmitted, and so does burn 1's,
#173,062.
Without chain access, from a clone of this repository, with Node.js 24 and Foundry:
git submodule update --init # the four pinned libraries of NOTICE.md
cd relayer
npm ci
npm run check -- --dir ../fixtures/v3-testnet-hero
# ...
# All checks passed: 31 passed, 0 failed.
npm run kit -- check --dir ../fixtures/v3-testnet-kit
# ...
# All checks passed: 41 passed, 0 failed.
npm test
# ℹ tests 403
# ℹ pass 403
# ℹ fail 0
# ℹ skipped 0
cd ../contracts
FOUNDRY_PROFILE=v3 forge build --skip test --skip script
forge test
# Ran 30 test suites in …: 370 tests passed, 0 failed, 2 skipped (372 total tests)
cd ../site
npm ci
npm run check
# ℹ tests 324
# ℹ pass 324
# ℹ fail 0npm run check re-verifies a fixture directory with no network access: every header hashes to its
block hash, every GRANDPA signature verifies and meets the threshold, every storage proof walks from
the state root to its value, and tampered copies fail. Each directory passes:
--dir |
What it holds | Checks |
|---|---|---|
../fixtures/mainnet |
old mainnet, set 3895, head #2,726,202 (rows 1 to 4) | 46 passed |
../fixtures/mainnet-pre-v3 |
old mainnet before the V3.0 launch, set 3896, head #2,727,094 | 46 passed |
../fixtures/v3-testnet |
V3 testnet, set 20, head #71,745 (rows 5 to 8) | 48 passed, 1 skipped |
../fixtures/v3-testnet-vault |
the vault's lock 0, proven at V3 #101,027 | 31 passed |
../fixtures/v3-testnet-mint |
a mint bundle for lock 0 at V3 #104,587 | 31 passed |
../fixtures/v3-testnet-hero |
the bundle row 32 minted with, V3 #108,424, set 30 | 31 passed |
../fixtures/v3-testnet-lock1 |
the bundle lock 1 was minted with (row 55), V3 #118,637, set 33 | 31 passed |
../fixtures/v3-testnet-set59 |
V3 testnet, set 59, head #214,510: the rollups' checkpoint and balance proof (rows 67, 68, 75 and 76) | 48 passed, 1 skipped |
../fixtures/v3-testnet-set75 |
V3 testnet, set 75, head #269,340: the checkpoint the BNB Smart Chain testnet's client starts from | 48 passed, 1 skipped |
../fixtures/westend-asset-hub-revive |
a pallet-revive contract slot on Westend Asset Hub | 31 passed |
npm run kit -- check does the same for the proof kit's fixture, ../fixtures/v3-testnet-kit: V3
#264,729 with its finality under set 73, 15 reads of balances, asset accounts, NFT items and
contract entries, the vault's 11 slots, and their tampers (41 passed; For builders).
FOUNDRY_PROFILE=v3 forge build writes the V3 build of the vault, which its tests check; without
it those tests are skipped. The 2 skipped tests are the Sepolia fork tests, which run with
SEPOLIA_FORK=1 against a Sepolia RPC. forge installs solc 0.8.27 on first use.
CI runs all of this on every push and pull request, on Linux: the
contracts, the relayer with the ten fixture directories and the kit's fixture, and the site; its
badge is at the top. On Windows, check the sources out with LF line ends (core.autocrlf false):
CRLF changes the libraries' source hashes, which the bytecode's metadata carries, so a build no
longer matches the deployed contracts.
The proof kit lets another EVM contract read Portaldot V3 state that the light client finalized, by the hash of a V3 block the light client imported: the state root comes from the light client, never from the caller, and the contract walks the storage proof from it. Everything is in docs/PROOF-KIT.md.
| Read | PortaldotStateReader function |
Gas at V3 #264,729: on Sepolia (eth_estimateGas, the deployed Reader) |
forge (a transaction) |
|---|---|---|---|
An account's nonce and balances (System.Account) |
accountInfo |
452,621 | 453,099 |
An asset balance and its status (Assets.Account) |
assetAccount |
480,180 | 480,441 (403,261 to 480,441 for three accounts) |
An NFT's owner (Nfts.Item) |
nftOwner |
363,589 | 364,768 |
A contract's slot, with its code hash (Revive.AccountInfoOf and its child trie) |
contractSlot |
877,450 | 874,583 |
- Through the Reader: a contract compiles only
contracts/src/kit/IPortaldotStateReader.soland theIFinalizedStateRoots.solit imports (both Solidity 0.8.27, exact), and callsPortaldotStateReaderon Sepolia,0x2ea6…1fA9(row 127): a stateless contract with no owner, bound toGrandpaLightClient#2 and linked to the deployedStorageProofs, source-verified. - With the library:
PortaldotStatehas the same reads as internal functions, the key builders and the decoders. - The example:
ProvenAssetGaterecords the EVM addresses that held at least a minimum of a V3 asset at a snapshot block. On Sepolia,0x24aB…8d23(row 128) takes at least 1 pUSD (asset 7777) at V3 #264,729, source-verified. No claim has been sent to it: no account of the project holds a V3 asset, so its claims are shown by its forge tests. - The tools: from
relayer/(afternpm ci),npm run kit -- read balance --account 0xD9838A86cF83C28Ee486544060D1cA4Ca2971071fetches a proof from the V3 node (or, with--from-fixture ../fixtures/v3-testnet-kit/state-reads.json, from the fixture), walks it off chain and has the deployed Reader make the same read byeth_call(read asset,read nft,read slotandread locklikewise);npm run kit -- checkchecks the fixture offline.
Proven: a value under the state root of a V3 block the light client imported. Trusted: V3's 2
validators, the light client's admin and checkpoint, V3's sudo key and runtime upgrades, and the
vendored ed25519, BLAKE2b and trie code, treated as unaudited. Testnet limits: on Oct 31 the V3
testnet finalized no block from 3:58 PM to 9:14 PM PKT, and its node answered HTTP 502 from about
9:17 PM for a while (by 10:32 PM it was finalizing again), so the examples took their proofs from
the fixture (--from-fixture); when the kit was deployed (9:13 PM PKT) the newest block the light
client on Sepolia held was #264,729, and every example reads at that block; the decoders are for
V3's runtime 3002.
Not in v0.1: an npm package or a packaged command line; the TypeScript is in relayer/ and runs
with npm run kit.
Works today
- Finality from two Portaldot chains, verified on Sepolia: the old mainnet with 7 of its 10 validators' signatures (rows 2, 3, 9 to 17 and 39 to 43) and the V3 testnet with both of its 2 (rows 6, 7, 18 to 23, 26 to 31, 44 to 46, 52 and 53). A flipped signature byte reverts (rows 4, 8 and 33).
- Storage proofs against a verified state root: an account balance on each chain, trie V0 and trie V1 (rows 3 and 7), and a pallet-revive contract slot in the vault's child trie (row 32).
- The relayer (
npm run relay) imports every validator-set change in order, then the finalized head, after a dry run of the whole plan on a Sepolia node (a rollup's node with--network; rows 9 to 23, 26 to 31, 39 to 46, 52 and 53, on the rollups rows 79 to 82, 88 to 93 and 102 to 105, and on the BNB Smart Chain testnet rows 142 to 144). - The relayer in watch mode since Oct 23 (UTC),
--watch 600, on a cloud server since Oct 31: a new run 10 minutes after each one ends, one process per chain, each with a key of its own (V3:0xE94a…0257, old mainnet:0x9926…C7f8). A run imports every set change it finds, and the head once it is--head-everyblocks past the client's latest header: 600 on V3 and 60 on the old mainnet (the defaults), about an hour of each chain's blocks. While it runs and Sepolia's base fee is below its gates (0.05 gwei for a head, 1.5 gwei for a set change), each of Sepolia's light clients is at most about 70 minutes behind its chain's finalized head, Sepolia's V3 client is at V3's current validator set within minutes of a change, and a mint on Sepolia needs only V3's finality and one transaction. Its transactions are indeployments/relay-sepolia-0xe94a….jsonlanddeployments/relay-sepolia-0x9926….jsonl, outside the numbered rows. - The V3 light client on two rollups, since Oct 27 (UTC): on Base Sepolia and Arbitrum
Sepolia, the same build verified V3 block #214,510 with both validators' signatures and proved an
account balance from its state root (rows 68 and 76), and the relayer, with
--network base-sepoliaorarbitrum-sepolia, imported V3's set change 59 → 60 into each (rows 79 and 81; more chains). - The vault on the V3 testnet and lock 0 (the vault).
- The bridge from V3 to Sepolia: BridgeMinter and wtPOT (rows 24 and 25), and one
importAndMintthat verified V3's finality, bound the vault, provedcommitments[0]and minted 1 wtPOT (row 32). A tampered signature, header, storage proof, amount or recipient reverts with the error that names it, and a replay withNonceAlreadyUsed(0)(rows 33 to 38). - Lock to mint in 48 seconds, measured: lock 1, sent on V3 and minted on Sepolia, the mint's block 36 seconds after the lock's by the block timestamps (rows 54 and 55).
- Auto-mint since Oct 27 (UTC),
npm run automint -- --send --watch 15(since Oct 28 one process for the three chains, with--network sepolia,base-sepolia,arbitrum-sepolia, and since Nov 1 for four, withbsc-testnet), from a key of its own that holds only gas: once V3 has finalized a new lock, and after a 90-second grace, it sends the lock's mint on the chain the lock names, for the lock's recipient, so a user signs only the lock and needs no ETH there on the way in. Lock 2 was minted this way, the mint's block 144 seconds after the lock's (rows 83 to 85). It can send only what a lock commits to: the minter checks the proofs and mints only to the committed recipient (auto-mint). Lock 3, sent from a Substrate (5…) account the way the app's Portaldot Wallet path sends a lock, was found inSystem.Eventsand minted the same way (rows 86 and 87, lock 3). - The way back, trusted: 0.5 wtPOT burned on Sepolia (row 47); 2 of the 3 committee members
approved its release on V3 through pallet-multisig, and the vault paid 0.5 tPOT to the address
the burn named (rows 48 to 51).
npm run releasechecks the burn, dry-runs the release from the multisig before anything is sent, and checks the payout (docs/BRIDGE.md). - The auto-release watcher and fast mode, since about 09:50 UTC on Oct 24:
release --watch 15 --confirmations 3 --send(since 05:28 UTC on Oct 28 with--network sepolia,base-sepolia,arbitrum-sepoliaand each chain's confirmations, Sepolia's still 3, and since Nov 1 withbsc-testnet, where it waits for the chain's finality), with members 1 and 2's keys, on the same machine as the relayer. Every 15 seconds it looks for new burns, gives each every check of the release tool, and sends both approvals. In fast mode it reads a burn at the block 3 below Sepolia's latest instead of waiting for Sepolia's finality, for a burn of at most 1 tPOT (its cap), with the fast releases whose burn is not final yet at most 1 tPOT in all (its budget); a larger burn waits for finality. Burn 1, 0.1 wtPOT, was released on V3 84 seconds after its Sepolia block, measured by the block timestamps (rows 56 to 58). Fast mode is as trusted as the standard way back (the committee), and a reorg deeper than 3 blocks could undo a burn after its release (docs/BRIDGE.md). - The way in to Base Sepolia and Arbitrum Sepolia: BridgeMinter and wtPOT on each rollup since
Oct 27 (UTC), for the same V3 vault, each with its rollup's own V3 light client (rows 94 to 97).
Since Oct 28
npm run mint(bundle,plan,mintandstatus) and auto-mint take--network, and auto-mint minted lock 4 on Base Sepolia and lock 5 on Arbitrum Sepolia, each first mint binding that rollup's minter to the vault (rows 98, 99, 106 and 107, the way in), then the app's locks 6 and 7 (rows 117 to 120). - The way back from Base Sepolia and Arbitrum Sepolia, since Oct 28 (UTC), trusted like Sepolia's:
npm run releaseand its watcher take--network, each rollup with its own records, its own finality (itsfinalizedblock tag, which follows Sepolia's finality of the batch: 16 to 22 minutes behind the rollup's latest block on Oct 28) and fast mode (36 seconds of its blocks, 180 on Base Sepolia and 144 on Arbitrum Sepolia), and one fast budget across the three chains: one vault, one committee, one watcher. 0.001 wtPOT was burned on each rollup (rows 108 and 109), and the watcher, running with--network sepolia,base-sepolia,arbitrum-sepoliasince 05:28 UTC, released both on V3, each burn final on its rollup: 0.001 tPOT each to the address the burn named (rows 110 to 113, the way back); the app's two burns followed in fast mode (rows 121 to 126). - The app's first transfers from an outside wallet, on Oct 28 (UTC), both ways on both rollups: the owner's own MetaMask wallet locked 1 tPOT for Base Sepolia and 1 for Arbitrum Sepolia from the app, auto-mint minted both, the wallet burned both from the app, and the watcher released both on V3 in fast mode (rows 114 to 126, the app's first transfers). The app has been public since Oct 31.
- The BNB Smart Chain testnet, both ways, since Nov 1 (UTC): the V3 light client,
BridgeMinterand wtPOT at the rollups' addresses (rows 129 to 141), kept at V3's validator set by the server's 20-minute relay runs (rows 142 to 144). Auto-mint minted lock 8 there, binding the vault, the mint's block 125 seconds after the lock's, and the watcher released burn 0 on V3 once it was final there, 46 seconds from the burn's block to the release's (rows 145 to 149, the BNB Smart Chain testnet). The app offers it as a fourth destination. - The bridge app, jonathan-moore58.github.io/bridge-zero/app (
site/app/), since Oct 31: lock and burn on the four chains from MetaMask or a Polkadot wallet, auto-mint's handoff with a mint built in the browser as the fallback (not run on chain yet), each transfer followed to its receipt, and a Verify tab that checks any transfer's signatures and proofs in the browser. - The status page, jonathan-moore58.github.io/bridge-zero (
site/): both light clients, the vault and the bridge read live, the recorded runs as the fallback when an RPC is down, and the six tampered mints checked live against BridgeMinter..github/workflows/pages.ymlbuilds and deploys both pages on every change to the records. - The proof kit, since Oct 31:
PortaldotState,PortaldotStateReaderand the exampleProvenAssetGateread V3 balances, asset balances, NFT owners and contract slots at a block the light client imported, tested on a real light client that imports V3 #264,729, andnpm run kitfetches, walks and checks the proofs (For builders). The Reader and the gate are on Sepolia, source-verified (rows 127 and 128): the Reader, bound toGrandpaLightClient#2 and linked to the deployedStorageProofs, returns the same values as the off-chain walk. - The operator's processes on a server since 17:45 UTC on Oct 31: both of Sepolia's relayers, the
release watcher, auto-mint, the rollups' relay runs on a timer and a health check every 15
minutes, as systemd services that start at boot. Auto-mint and the watcher restart a minute after
a failure, the watcher not after its stop for good (exit code 4: a halt, a key that does not
open); a relayer restarts only after a signal (an out-of-memory abort, a kill), and one that stops
on an error waits for the operator, with the health check failing until then
(ops/server/). Its first rollup relay run imported V3 #266,274 into Base Sepolia's
client at 17:50:50 UTC
(
0x871a603d…) and #266,278 into Arbitrum Sepolia's at 17:51:13 (0xa40c202b…), outside the numbered rows. - Deploy it yourself: docs/DEPLOY.md takes a copy of one's own from the build and tests to its light clients, vault, committee, minters and pages, with each command as the tools take it; no copy has been deployed from other accounts yet.
- CI since Oct 31:
.github/workflows/ci.ymlbuilds and tests the contracts, the relayer (with the ten fixture directories and the kit's fixture) and the site on every push and pull request. - Tests: 370 forge tests (the 2 Sepolia fork tests skipped), 403 relayer tests, 324 site tests and
432 fixture checks (391 with
npm run check, 41 withnpm run kit -- check) pass, in CI and in Verify it yourself.
Known limits
- The V3 testnet has 2 GRANDPA validators, probably development keys: the V3 path shows the mechanics. The old mainnet rows (2, 3, 9 to 17, 39 to 43) show the same checks against a real 10-validator set. Both chains have a sudo key that can change any storage; a light client cannot rule that out.
- The return path is trusted: whoever holds 2 of the 3 committee keys can release up to the vault's outstanding tPOT, to any address, or release nothing after a burn, and nothing on V3 checks a release against a burn. On the testnet one operator holds all three keys: like the V3 validators, the committee shows the mechanics, not independent approvers. The watcher holds members 1 and 2's keys on one machine, so whoever controls that machine and its password file can release up to the vault's outstanding tPOT.
- Each light client's admin (the deployer) set its checkpoint and can pause it and install a new checkpoint while it is paused or halted. Since it can pause at any time, it could make the client, and so the minter, accept a state root of its choosing: the admin is trusted like the validator set. In production it would be a timelock or a multisig, or renounced.
- Testnet only: every contract is on Sepolia, Base Sepolia, Arbitrum Sepolia, the BNB Smart Chain testnet or the V3 testnet, and the vendored ed25519, BLAKE2b and trie code is treated as unaudited (docs/GAS.md).
- Gas, against Sepolia's 16,777,216 cap per transaction: a V3 head import about 1.87M, a V3 set
change about 2.39M, lock 0's mint with the bind 2,952,838 (17.6%), lock 1's without it 2,391,069 (14.3%), an
old-mainnet set change
with 7 signatures 6,322,745 (37.7%); each signature costs about 0.77M. A set change to more
than 16 validators (
MAX_AUTHORITIES) halts the client, since from 18 new keys up it would not fit one transaction. Sepolia's fees are near zero (the mint paid 1,015 wei per gas); on Ethereum mainnet the same gas is paid at mainnet prices. On the BNB Smart Chain testnet, where every transaction pays a 0.1 gwei tip, a V3 set change took 1.85M gas and a head 1.69M (rows 142 to 144), and lock 8's first mint 2,658,627 (row 146). - The relayer, auto-mint and the release watcher run on one machine: until Oct 31 a laptop that
started them at logon, and since 17:45 UTC on Oct 31 a small cloud server that runs them as
services that start at boot and restart after most failures (a relayer that stops on an error, and
the watcher after its stop for good, exit code 4, wait for the operator)
(ops/server/). A light client is as current as its last run. While that machine is
off, no relay run, mint or release happens: a new lock waits for auto-mint, though anyone can
still send its mint (
npm run mint), and a burn waits until a member runs the release by hand or the watcher starts again. The laptop's longest gaps, in UTC, by the relayer's histories (indeployments/to Oct 28) and the laptop's power log: no import into any light client from 00:38 to 06:56 on Oct 24, while it slept (00:45 to 06:52); from 21:04 on Oct 24 to 16:28 on Oct 26, while it was off after an unexpected shutdown (until 16:08); from 21:36 on Oct 26 to 07:08 on Oct 27, while it slept (22:25 to 07:07); from 16:21 on Oct 28 to 20:05 on Oct 30, while it slept (17:13 on Oct 28 to 19:54 on Oct 30, but for 15 seconds at 17:51); and from 21:08 on Oct 30 to 06:36 on Oct 31, while it slept (21:42 to 06:34). No lock or burn was waiting in any of them, and after each the relayer had every client at its chain's validator set within 45 minutes of the laptop's return. The rollups' V3 clients, and since Nov 1 the BNB Smart Chain testnet's, are kept at V3's validator set by relayer runs on a timer, not by a relayer in watch mode. Until about 20:30 UTC on Oct 31 that was one run for each rollup at 02:00, 08:00, 14:00 and 20:00 UTC and 20 and 40 minutes after each, set for the set changes V3 made at 01:54, 07:54, 13:54 and 19:54 until Oct 28. Each time V3 stopped making blocks its set changes moved: after its stop from 12:45 on Oct 28 to 02:09 on Oct 29 they came at 02:54, 08:54, 14:54 and 20:54, and after the stop of Oct 31 (10:58 to 16:14) set 75 came at 18:54, so a lock for a rollup that came after a set change could wait hours for the next run. Since about 20:30 UTC on Oct 31 a run for each rollup every 20 minutes imports a set change the client lacks within about 20 minutes, and a head once the client is 1,800 V3 blocks (about 3 hours) behind; anyone can import the change sooner withnpm run relay -- --network base-sepolia --chain v3 --send --account <keystore> --password-file <path>(orarbitrum-sepolia), from a funded key of their own. - The way back, standard, waits for Sepolia's finality before the committee releases: 12.5 to 19.9 minutes after a block by the watcher's 15-second rounds (median 16.0, over the 2,221 blocks it saw arrive and become final on Oct 24; the chain's own floor is two epochs, 12.8 minutes). The recorded run, released by hand, took 19 minutes 42 seconds from the burn's block to the release's (rows 47 to 51). Fast mode took 84 seconds from the burn's block to the release's, measured (rows 56 to 58), but only for a burn of up to 1 tPOT, still trusted (the committee), and a reorg deeper than 3 blocks could undo a burn after its release. The way in needs only V3's finality: 48 seconds for lock 1. With auto-mint the mint's block came 144 seconds after the lock's for lock 2, and 139 and 133 seconds after it for locks 6 and 7 on the rollups (rows 117 to 120), since auto-mint waits a 90-second grace after finality so that a user minting from the page is not raced; auto-mint skips locks below 0.001 tPOT and keeps to 0.02 ETH of gas a day on Sepolia, 0.002 on each rollup and 0.005 tBNB on the BNB Smart Chain testnet. LP fast-fill is planned for the way back. From a rollup the standard way back waits for that chain's finality, 16 to 22 minutes behind its latest block on Oct 28 (Sepolia finalizes the rollup's batch), and fast mode for 36 seconds of its blocks: 105 seconds from Arbitrum Sepolia's burn block to the release's (rows 121 to 123), and 905 seconds from Base Sepolia's, which waited for the shared fast budget (rows 124 to 126). Until its batch is on Sepolia a rollup's sequencer alone orders its latest blocks, and a restart or a failover that drops them could undo a burn released fast. The fast budget, 1 tPOT, counts every chain's fast releases. From the BNB Smart Chain testnet the watcher waits for that chain's finality, about a second, with no fast mode: 46 seconds from the burn's block to the release's (rows 147 to 149).
- On the BNB Smart Chain testnet the one public RPC that serves
eth_getLogsover a wide range (publicnode) keeps logs and receipts for recent blocks only, 10.5 to 12.7 hours of them on Nov 1, and answers older ones with none and no error. A burn the watcher saw is still released however late it qualifies, read again from its receipt on the official endpoints, which keep receipts; a burn the watcher never saw while its log was served (the watcher down for longer than that) waits for an RPC that keeps the minter's logs (docs/BRIDGE.md). - The proof kit reads only blocks the light client imported: when the kit was deployed on Oct 31
(9:13 PM PKT) the newest on Sepolia was #264,729, and the V3 testnet had finalized no block since
3:58 PM that day (it was finalizing again by 10:32 PM). An absent key reverts
KeyNotPresent, a proof that it holds nothing, which a consumer may treat as zero (for a contract's slot, only after pinning the contract: Absent values). The Reader is on Sepolia only, and the TypeScript is not an npm package.
Plans, not promises: a step counts once it runs on chain and is in the evidence. Who the bridge is for, the fees it could charge, its running costs and the three months after the event are in docs/BUSINESS.md: month 1 from Nov 10, month 2 from Dec 10, month 3 from Jan 10 to Feb 9.
Nov 10 to Feb 9: the three months after the event
- LP fast-fill, the first part of a liquidity engine. A liquidity provider pays a burn's
recipient on V3 within seconds, from its own tPOT; once the burn is final, the committee releases
the burn's amount from the vault to the provider that filled it. The vault's
releasepays any address, so the contracts stay as they are. The provider carries the wait and the reorg risk and can charge for it, as the liquidity bridges on Ethereum mainnet do. Month 1, first with its time windows kept off chain, then settled by a small contract on V3. - Fast-fill for the way in. A provider pays wtPOT on Sepolia at once and is repaid when the lock's proven mint lands. This needs new contracts on both sides: a vault that puts the fee the user allows into the lock's commitment, a minter that mints to the provider that filled, and a new wtPOT, because wtPOT's minter is fixed when it is deployed. Month 2, in a v2 vault and minter that also take a bridge fee.
- More EVM chains. The same contracts can be deployed on EVM chains whose transactions can call
the BLAKE2F precompile (EIP-152) and that have room for the light client's deployment
(15,039,367 gas on Sepolia). On Oct 24 the whole sequence (deploy, initialize, import a
validator-set change, prove a balance, and a tampered proof that must revert) passed in a
simulation, with no transaction sent, on Base Sepolia, OP Sepolia, Arbitrum Sepolia, Polygon
Amoy, the BNB Smart Chain testnet and Avalanche Fuji. On Oct 27 the V3 light client went live on
two of them, proof only: on Base Sepolia and on Arbitrum Sepolia,
GrandpaLightClient#2 verified a V3 block andProvenBalance#2 proved an account balance from its state root, and the relayer then imported V3's set change 59 → 60 into each (rows 59 to 82, more chains), andBridgeMinterand wtPOT followed on both the same day, bound to the same V3 vault (rows 88 to 97). On Oct 28 one auto-mint process minted the first lock on each, binding the vault there (rows 98 to 107, the way in), and the way back from the rollups followed the same day (rows 108 to 113, the way back), then the owner's own wallet used both routes through the app (rows 114 to 126). On Nov 1 the BNB Smart Chain testnet followed, both ways, the first destination that is not a rollup of Sepolia (rows 129 to 149, the BNB Smart Chain testnet); OP Sepolia, Polygon Amoy and Avalanche Fuji are still to come. Linea Sepolia is not on the list: its node answers the precompile, but its sequencer refuses every transaction that calls it, so Linea would need blake2b written in Solidity. zkSync Era Sepolia does not have the precompile. Each chain gets its own light client and minter; a lock's commitment already names the destination chain and minter, so one vault on Portaldot can serve them all, and the liquidity providers can serve every route. Base is the first mainnet planned: its contracts in month 2, with no value on them before month 3's audit and caps. - The proof kit as packages. An npm package of its TypeScript and a command line that re-checks a mint or proves any key, with an import mode for blocks the light client does not hold yet; the Reader on more chains. Month 1.
- BEEFY. A prototype verifier of the BEEFY finality that V3 also runs, signed with ECDSA keys
that the EVM checks with its
ecrecoverprecompile instead of ed25519 in Solidity: cheaper imports, under the same validators. Month 2. - Portaldot's V3 mainnet. The Portaldot Node Console lists a network named "PortaldotV3 Live" that finalizes with GRANDPA the same way (2 validators, checked read-only on Oct 20), and Portaldot's own bridge connects it to BNB Smart Chain; a light client started from its own checkpoint could follow it. Month 2.
- Before any mainnet value: an outside audit; each light client's admin replaced by a timelock or a multisig, or renounced; committee keys held by independent members; caps on the total value and on each transfer. Month 3, with a guarded mainnet launch.
Later
- A proven way back. An Ethereum light client on Portaldot, so that the vault checks a burn instead of trusting the committee. After Feb 9, not scheduled yet.
- Non-EVM chains. Solana is not an EVM chain: it would need its own verifier program. Research only.
Done
- A web app for the bridge. Lock and burn from a wallet, with auto-mint's mint in between, and a Verify page that checks any transfer's signatures and proofs in the browser: public since Oct 31. Its first transfers, from the owner's own wallet, ran on Oct 28 (rows 117 to 126, after its funding in rows 114 to 116). It offers four destinations: Sepolia, the two rollups and the BNB Smart Chain testnet, the last after its first round trip on Nov 1 (rows 129 to 149). Its fallback, a mint sent from the wallet with the proofs built in the browser, has not run on chain yet.
- A faster way back. Fast mode, in which the committee releases a burn of up to 1 tPOT after 3 Sepolia confirmations instead of Sepolia's finality, a trade-off it states, and the auto-release watcher, which runs both approvals as soon as a burn qualifies. On Oct 24 it released burn 1 on V3 84 seconds after the burn's Sepolia block (rows 56 to 58), where burn 0, released by hand after Sepolia's finality, took 19 minutes 42 seconds (rows 47 to 51); on Oct 28 a burn from Arbitrum Sepolia took 105 seconds (rows 121 to 123).
- The proof kit, packaged for other builders. The light client, the proof libraries and the
capture and check tools, gathered so that another EVM contract can read proven V3 state:
PortaldotStateReaderand the exampleProvenAssetGate, deployed on Sepolia on Oct 31 and source-verified (rows 127 and 128), thePortaldotStatelibrary, andnpm run kit(For builders). - The operator's processes on a server. Since 17:45 UTC on Oct 31 the relayers, the release watcher, auto-mint and the rollups' relay runs are services on a small cloud server, in place of a laptop (ops/server/).
From the git history, in UTC (the time zone Etherscan shows). Rows are the evidence rows.
- Oct 10 to 11, before the window. Read-only feasibility checks, from scripts kept outside this repository (Third-party code); nothing was signed or sent.
- Oct 14. The repository and its Foundry project; the capture tool and the off-chain checker; old-mainnet fixtures (a finality proof, a set change and an account proof, set 3895).
- Oct 15. The gas benchmark (one transaction fits; crypto-lib's ed25519 kept), then the light client: SCALE, header and justification decoding, verified header import, set changes and halts, trie V0 and V1 proofs, ProvenBalance. Split into linked libraries so every deployment fits Sepolia's per-transaction cap, measured on a Sepolia node; the read-only node probe.
- Oct 17. The V3 testnet snapshot (set 20, head #71,745), with the light client and trie V1 proofs tested on it. On chain: eight contracts on Sepolia, both light clients initialized, a verified block and a proven balance from each chain, and two tampered copies reverted (rows 1 to 8).
- Oct 18. A pallet-revive contract slot proven from a state root (Westend Asset Hub). The relayer: its first run brought both light clients to the live finalized heads (rows 9 to 23).
- Oct 19. The vault, its V3 tool and the decisions behind them (docs/DECISIONS.md). On chain: the vault deployed on the V3 testnet at block #100,955 and lock 0 at #101,009. Then BridgeMinter, wtPOT and the mint tool (docs/BRIDGE.md). On chain: both deployed, the V3 light client brought to the head, lock 0 minted in one transaction, five tampered copies and a replay reverted (rows 24 to 38).
- Oct 20. The release tool and the committee's rules (docs/BRIDGE.md). On chain: both light
clients brought to the live heads again (rows 39 to 46), then the trusted return path, run once:
0.5 wtPOT burned on Sepolia (row 47), committee members 1 and 2 funded, and the release opened by
member 1 and run by member 2 on V3 (rows 48 to 51). Then the relayer imported V3's next
validator set, and lock 1 was sent on V3 and minted on Sepolia 48 seconds later (rows 52 to 55).
Later, both light clients relayed again (V3 #121,505, set 33; old mainnet #2,734,895, set 3907),
and the live, read-only page (
site/) deployed to GitHub Pages by.github/workflows/pages.yml. - Oct 21. The Bridge Zero mark and favicon on the page.
- Oct 23. After Portaldot's V3.0 announcement, a check that the V3 testnet is the same chain
(the same genesis, block #100,955's hash, the vault's code and lock 0's log), then both light
clients brought to the live heads: V3 sets 33 to 46 and head #165,089 (14 transactions), old
mainnet sets 3907 to 3913 and head #2,739,249 (7 transactions, 7 of 10 signatures each). These
runs are in
deployments/relay-sepolia-0xd983….jsonl, outside the numbered rows. - Oct 24. The relayer in watch mode, one key per chain: on V3 since Oct 23, 22:15, and on the
old mainnet since 23:36, once its key was funded. By 07:57 it had imported V3 sets 46 to 48 and
4 V3 heads, and old mainnet set 3913 to 3914 and 4 heads (11 transactions). The page reads every
sender's history in
deployments/. Fast mode and the auto-release watcher (docs/BRIDGE.md), from 23:11 on Oct 23 to 09:43, with 213 relayer tests. On chain: the watcher running from about 09:50 with 3 confirmations, then burn 1, 0.1 wtPOT, on Sepolia at 09:51:48, and its release opened by member 1 at 09:52:42 and run by member 2 at 09:53:12 on V3, both sent by the watcher: 84 seconds from the burn's block to the release's (rows 56 to 58). Later that day: the business model (docs/BUSINESS.md); the light client's whole sequence simulated on seven other EVM testnets, of which six passed (Linea Sepolia's sequencer refuses every transaction that calls BLAKE2F); the committee's multisig funded with 40 tPOT more, 52.317 in all; and, on a branch,app, the bridge app's first screens. From about 21:10 the laptop that ran the relayer was off, until Oct 26 (Known limits). - Oct 26. The laptop on again at 16:08; the relayer had both clients at their chains' sets by
16:37. On the
appbranch: the wallet on wagmi's core and viem, with every installed wallet found through EIP-6963; the way-in proof built in the browser, the relayer's SCALE, trie, GRANDPA and calldata code ported, so that a mint from the app need not wait for the relayer (a path not yet run on chain); and the app's sends, the lock on V3, the mint and the burn, each dry-run before the wallet is asked and followed to its receipt (127 site tests). - Oct 27. The deploy script and the relayer take
--network base-sepolia|arbitrum-sepolia, with a V3 checkpoint at set 59. On chain: the deployer funded on both rollups through their canonical bridges, then on each the four libraries,GrandpaLightClient#2 andProvenBalance#2, a V3 block verified and a balance proven, and the relayer's first imports (rows 59 to 82). Auto-mint (npm run automint): lock 2 minted for its recipient from auto-mint's own key, the mint's block 144 seconds after the lock's (rows 83 to 85; 263 relayer tests). The Apache-2.0 licence for the project's own code. Locks sent as Substrate extrinsics, found inSystem.Events, and lock 3 minted that way (rows 86 and 87);BridgeMinterand wtPOT on both rollups, and both rollup clients at V3's set 62 (rows 88 to 97; 288 relayer tests). On theappbranch, rebased on main: Polkadot wallets (the Portaldot Wallet, polkadot{.js}, SubWallet, Talisman), whose lock is arevive.callsigned in the wallet (169 site tests). - Oct 28. The way in to the rollups: auto-mint serves several networks from one process, and
the mint tool takes
--network(309 relayer tests); one auto-mint process for the three chains minted lock 4 on Base Sepolia and lock 5 on Arbitrum Sepolia, each first mint binding the vault (rows 98 to 107). The way back from them:releaseand its watcher take--network, one watcher serving the three chains, with each chain's finality and fast mode and one fast budget; burn 0 on each rollup, released on V3 by the watcher once final (rows 108 to 113; 346 relayer tests). The rollups' relay runs on a schedule from 08:00. On theappbranch: Base Sepolia and Arbitrum Sepolia as destinations, both ways (240 site tests). From 11:05 to 11:40, with the app run locally, the owner's own wallet locked 1 tPOT for each rollup, auto-mint minted both, the wallet burned both, and the watcher released them on V3 in fast mode (rows 114 to 126, recorded on Oct 30). From 17:13 the laptop slept, until Oct 30 (Known limits). - Oct 30. The laptop awake from 19:54; the relayer had every client at its chain's validator set by 20:25, Sepolia's V3 client from set 64 to 71 and head #257,129 in one run (8 transactions). The release watcher, which had stopped when the laptop woke (polkadot-js's runtime-version subscription threw its 60 s request timeout from a timer no round handles), started again at 20:05. The records of the owner's transfers (rows 114 to 126; 126 numbered transactions).
- Oct 31. The Verify page: pick a transfer, and the browser checks V3's validator signatures,
the lock's storage proof and the commitment, then the destination's own verdict (287 site
tests). The
appbranch merged into main, and the app public from about 11:30. The release watcher logs an error no round handles and goes on (352 relayer tests). The proof kit:npm run kit, a fixture at V3 #264,729 (41 checks),PortaldotState,PortaldotStateReaderandProvenAssetGatewith their forge tests, and docs/PROOF-KIT.md (370 forge tests); on chain, the Reader and the gate on Sepolia at 16:13 and 16:15 (rows 127 and 128). That day the V3 testnet finalized no block from 10:58 to 16:14; its node then answered HTTP 502 for a while and at about 17:05 briefly served another chain, which the kit now refuses (382 relayer tests); by 17:32 it was finalizing again. At 17:45 the relayers, the release watcher and auto-mint moved to a small cloud server, as services (ops/server/); its first rollup relay run imported V3 #266,274 into Base Sepolia at 17:50:50 and #266,278 into Arbitrum Sepolia at 17:51:13. Then CI on every push, the deployment guide (docs/DEPLOY.md), the site's packages and fonts in NOTICE.md, and the rollups' sixteen contracts source-verified on Basescan and Arbiscan (20:16 to 20:20). From about 20:30 the server runs the rollups' relays every 20 minutes, as V3's set changes had moved (Known limits). - Nov 1. The BNB Smart Chain testnet as a fourth network in the tools and, hidden, in the app's
config, with its runbook, ops/bnb-activation.md, merged into main with
the app's fixes of Oct 31 (the evidence list's chains and blocks, the rollups' measured times, no
fixed V3 set-change times): 403 relayer tests and 316 site tests. With tBNB from 07:54, the
deployer sent the light client from 08:56, the relayer key's funding,
BridgeMinterand wtPOT, and the auto-mint key's funding by 09:06 (rows 129 to 141). The server's units took the chain from 09:20, and the relayer's first run there brought the client to V3's set 77 at 09:23 (rows 142 to 144). Lock 8 at 09:25:42, minted by auto-mint at 09:27:46; burn 0 at 09:30:07, released on V3 by the watcher at 09:30:54 (rows 145 to 149). The app shows it as a fourth destination, with its measured times, 125 seconds in and 46 back.
Sepolia, from 0xD983…1071, blocks 11,934,321 to 11,934,336. Every transaction is built from
the captured chain data in fixtures/, each step is recorded in
deployments/sepolia.json, and
node contracts/script/deploy-sepolia.mjs replays the plan on a Sepolia node without sending
anything. All eight have their source on Etherscan: six as an Exact Match, and GrandpaLightClient
#2 and ProvenBalance #2 as a Similar Match to #1, whose source they share.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 1 | Old mainnet client initialized at a checkpoint: set 3894, its 10 keys (the one trusted, admin step) | 0xc4d1d212… |
11,934,329 | success |
| 2 | Old mainnet set change 3894 → 3895 verified: 7 of 10 validator signatures | 0x6851aa04… |
11,934,330 | success, 6,322,745 gas |
| 3 | Old mainnet block #2,726,202 verified (7 of 10), and an account balance proven from its state root (System.Account, trie V0) | 0x8ddd807e… |
11,934,331 | success, 6,254,403 gas |
| 4 | Row 3 with one signature byte flipped | 0x0062bc6c… |
11,934,332 | reverted: InvalidSignature(0, 0) |
| 5 | V3 testnet client initialized at a checkpoint: set 19, its 2 keys (the one trusted, admin step) | 0x076e061f… |
11,934,333 | success |
| 6 | V3 testnet set change 19 → 20 verified: 2 of 2 validator signatures | 0xbcd75f28… |
11,934,334 | success, 2,394,089 gas |
| 7 | V3 testnet block #71,745 verified (2 of 2), and an account balance proven from its state root (System.Account, trie V1) | 0xd4c6fef3… |
11,934,335 | success, 2,298,856 gas |
| 8 | Row 7 with one signature byte flipped | 0xddb3c231… |
11,934,336 | reverted: InvalidSignature(0, 0) |
| Contract | Old Portaldot mainnet | Portaldot V3 testnet |
|---|---|---|
GrandpaLightClient |
0x3897…0ab6 |
0x4471…dcb0 |
ProvenBalance |
0x40f4…86f0 |
0x5b41…ad3c |
Linked libraries: SCL_EIP6565 0x51da…b77e (ed25519), GrandpaCodec 0x482a…dfd2,
AuthoritySets 0xf2cd…e6d2, StorageProofs 0x5da0…f2a3.
initializeis the one trusted step: the admin sets the first validator set and a checkpoint header. After it, a header is accepted only with enough valid signatures from the current set.- The V3 testnet has 2 GRANDPA validators.
- Rows 1 to 8 use captured blocks (old mainnet: Oct 14; V3 testnet: Oct 17).
Sepolia blocks 11,943,046 to 11,943,083, Oct 18 (UTC), sent by the relayer
(npm run relay -- --chain mainnet|v3, in relayer/). A run reads the light client's
state on Sepolia, finds every validator-set change on the Portaldot chain since the client's latest
block, and imports them in order, each with the justification the chain stores for it. It then
imports the finalized head. Before anything is signed, the whole plan runs on a Sepolia node with
eth_simulateV1. Each transaction is recorded in
deployments/relay-sepolia-0xd983….jsonl.
All 15 cost 0.00000007 Sepolia ETH in total.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 9 | Old mainnet set change 3895 → 3896 at #2,726,491: 7 of 10 signatures | 0xf9cfea23… |
11,943,046 | success, 6,325,153 gas |
| 10 | Old mainnet set change 3896 → 3897 at #2,727,211: 7 of 10 signatures | 0xb960fa66… |
11,943,047 | success, 6,326,980 gas |
| 11 | Old mainnet set change 3897 → 3898 at #2,727,931: 7 of 10 signatures | 0xffd4427e… |
11,943,048 | success, 6,321,564 gas |
| 12 | Old mainnet set change 3898 → 3899 at #2,728,651: 7 of 10 signatures | 0x699bf194… |
11,943,050 | success, 6,322,253 gas |
| 13 | Old mainnet set change 3899 → 3900 at #2,729,371: 7 of 10 signatures | 0xff2f97ec… |
11,943,051 | success, 6,322,726 gas |
| 14 | Old mainnet set change 3900 → 3901 at #2,730,091: 7 of 10 signatures | 0x055c6793… |
11,943,052 | success, 6,312,979 gas |
| 15 | Old mainnet set change 3901 → 3902 at #2,730,811: 7 of 10 signatures | 0x8d905bdf… |
11,943,053 | success, 6,322,906 gas |
| 16 | Old mainnet set change 3902 → 3903 at #2,731,531: 7 of 10 signatures | 0x4a7ae5fe… |
11,943,054 | success, 6,323,348 gas |
| 17 | Old mainnet block #2,731,989 verified (7 of 10): the live finalized head | 0xffa0182e… |
11,943,055 | success, 5,791,275 gas |
| 18 | V3 testnet set change 20 → 21 at #75,119: 2 of 2 signatures | 0x9ea49f1c… |
11,943,078 | success, 2,391,213 gas |
| 19 | V3 testnet set change 21 → 22 at #78,695: 2 of 2 signatures | 0xc3c4fd71… |
11,943,079 | success, 2,394,850 gas |
| 20 | V3 testnet set change 22 → 23 at #82,282: 2 of 2 signatures | 0xb20bfd53… |
11,943,080 | success, 2,392,394 gas |
| 21 | V3 testnet set change 23 → 24 at #85,859: 2 of 2 signatures | 0xf61beb52… |
11,943,081 | success, 2,392,238 gas |
| 22 | V3 testnet set change 24 → 25 at #89,440: 2 of 2 signatures | 0x65e47bcb… |
11,943,082 | success, 2,392,423 gas |
| 23 | V3 testnet block #92,604 verified (2 of 2): the live finalized head | 0x493d1fb0… |
11,943,083 | success, 1,878,952 gas |
The old mainnet changes its validator set every 720 blocks (about 12 hours) and keeps the same 10 keys; the V3 testnet does so about every 6 hours with its 2 keys. Every change has to be imported in order: a client that skips one can no longer verify the next set's signatures.
PortaldotVault holds tPOT locked for the bridge:
anyone can lock, naming the chain and minter that may mint for it, and only the committee's 2-of-3
multisig account can release (the trusted return path). Deployed on Oct 19 by
npm run v3-vault -- deploy; the record is deployments/v3-testnet.json,
and docs/V3.md and docs/DECISIONS.md explain the costs and
the design.
| Vault | 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd (899 bytes of EVM bytecode, solc 0.8.27) |
| Deployed by | 5ERJiUnzmQwEF98X3rGyKiNsyjrcenLGJST3MULCjUSwgPJ7 through revive.instantiateWithCode |
| Extrinsic | 0xd10c71ba51591c42706b98ec853cc5d584135485f0a104178ecb5eb87033dff9, block #100,955 0x2c488560…d1035251 |
| Cost | storage deposit 83.232 tPOT, fee 0.1285 tPOT |
| Releaser | 0xA08A7f7DEd49CC81F901Fb42Fec00edFC48c0fc1, the 2-of-3 multisig 5FTrjz…xPk3 (trusted) |
Check it: cast code 0xa02EE16EB03d2cf1aeF199F7658FB8c8E3188ffd --rpc-url https://test-evm-rpc-v3.portaldot.dev
returns the code, and npm run v3-vault -- status reads the vault.
Lock 0, Oct 19: 1 tPOT from 0xD983…1071 for the same address on Sepolia, naming the minter
0x8287…cB9E. V3 transaction 0x2516e421d29e5bcb0032253dc66db87acc3cc5b22b021252fcb7c2cfdad56051,
block #101,009; the vault stores its commitment 0x9eba08c9…41e23435 in commitments[0].
BridgeMinter mints wtPOT for a lock only after proving
it: in one transaction it imports the V3 block through the light client (GRANDPA signatures), binds
the vault from its Revive.AccountInfoOf entry the first time, reads commitments[nonce] from the
vault's child trie against that block's state root, recomputes the commitment, and mints to the
recipient it names. Nobody is trusted to report the lock. docs/BRIDGE.md explains
the proofs and the runbook; npm run mint (in relayer/) captures the bundle, dry-runs
every transaction on a Sepolia node first, and sends. The record is
deployments/sepolia-bridge.json; the bundle the mint used is
fixtures/v3-testnet-hero.
| Contract (Sepolia, source-verified) | Address |
|---|---|
BridgeMinter |
0x8287…cB9E |
WrappedTPOT (wtPOT, 14 decimals) |
0x1fcB…Cb14 |
Sepolia blocks 11,950,851 to 11,950,922, Oct 19 (UTC):
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 24 | BridgeMinter created (the deployer's nonce 31, the address lock 0 names) | 0x29a5a698… |
11,950,851 | success, 9,806,643 gas |
| 25 | WrappedTPOT created, the minter its only minter | 0xbf017d71… |
11,950,852 | success, 3,183,649 gas |
| 26 | V3 testnet set change 25 → 26 at #93,035: 2 of 2 signatures | 0xea5cdc9f… |
11,950,889 | success, 2,387,559 gas |
| 27 | V3 testnet set change 26 → 27 at #96,612 | 0x08fccf25… |
11,950,890 | success, 2,391,165 gas |
| 28 | V3 testnet set change 27 → 28 at #100,199 | 0x1a6139a2… |
11,950,891 | success, 2,396,029 gas |
| 29 | V3 testnet set change 28 → 29 at #103,784 | 0x08980606… |
11,950,893 | success, 2,396,053 gas |
| 30 | V3 testnet set change 29 → 30 at #107,359 | 0xd8d6c745… |
11,950,895 | success, 2,390,572 gas |
| 31 | V3 testnet block #108,379 verified: the light client at the live head | 0xf7a9aaa7… |
11,950,896 | success, 1,869,963 gas |
| 32 | Lock 0 minted: V3 block #108,424 verified (2 of 2), the vault bound, commitments[0] proven, 1 wtPOT to 0xD983…1071 |
0x91714c84… |
11,950,902 | success, 2,952,838 gas |
| 33 | Row 32 with one byte of a precommit's signature flipped | 0xa3f00f83… |
11,950,910 | reverted: InvalidSignature(0, 0) |
| 34 | Row 32 with one byte of the header's state root flipped | 0x3e6ba7c8… |
11,950,912 | reverted: FinalityProofBlockMismatch |
| 35 | Row 32 with one byte of the storage proof flipped | 0x3ae2c269… |
11,950,915 | reverted: Incomplete Proof! |
| 36 | Row 32 claiming 1,000 wtPOT instead of 1 | 0x3d3f5130… |
11,950,917 | reverted: CommitmentMismatch |
| 37 | Row 32 for another recipient | 0xa2b9691f… |
11,950,920 | reverted: CommitmentMismatch |
| 38 | Row 32 sent again | 0x3dab3abc… |
11,950,922 | reverted: NonceAlreadyUsed(0) |
- The V3 testnet's 2 validators are probably development keys, so this path shows the mechanics; rows 2 to 4, 9 to 17 and 39 to 43 show the same checks against the old mainnet's real 10-validator set.
- The return path is trusted: wtPOT burned on Sepolia is released on V3 by the committee's 2-of-3 multisig, which the vault takes as its only releaser.
Sepolia blocks 11,955,153 to 11,955,172, Oct 20 (UTC): one run for each light client, recorded in the same file as rows 9 to 23. The 8 transactions cost 0.00000004 Sepolia ETH in total.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 39 | Old mainnet set change 3903 → 3904 at #2,732,251: 7 of 10 signatures | 0x6aa1731a… |
11,955,153 | success, 6,325,583 gas |
| 40 | Old mainnet set change 3904 → 3905 at #2,732,971 | 0xed7d6227… |
11,955,154 | success, 6,320,769 gas |
| 41 | Old mainnet set change 3905 → 3906 at #2,733,689 | 0x5bd7663b… |
11,955,155 | success, 6,321,079 gas |
| 42 | Old mainnet set change 3906 → 3907 at #2,734,409 | 0x24327136… |
11,955,156 | success, 6,324,584 gas |
| 43 | Old mainnet block #2,734,439 verified (7 of 10): the live finalized head | 0x00a39857… |
11,955,157 | success, 5,788,312 gas |
| 44 | V3 testnet set change 30 → 31 at #110,943: 2 of 2 signatures | 0x4e626529… |
11,955,170 | success, 2,392,411 gas |
| 45 | V3 testnet set change 31 → 32 at #114,535 | 0x8ec46539… |
11,955,171 | success, 2,386,966 gas |
| 46 | V3 testnet block #116,992 verified: the light client at the live head | 0x7be3e419… |
11,955,172 | success, 1,883,708 gas |
BridgeMinter.burn burns wtPOT and emits BurnRequested with a burn id; on V3 the committee's
2-of-3 multisig calls the vault's release, which pays the tPOT once per burn id. Nothing on V3
checks the burn: the committee is trusted. npm run release (in relayer/) dry-runs
the burn on a Sepolia node and the release from the multisig before anything is sent, and checks
the payout after; docs/BRIDGE.md is the runbook. The records
are deployments/sepolia-bridge.json (the burn) and
deployments/v3-testnet.json (committeeFunding and releases,
with every extrinsic's full hash). Oct 20 (UTC), rows 47 to 51, and in fast mode on Oct 24, rows 56
to 58:
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 47 | Burn 0 on Sepolia: 0.5 wtPOT burned for 0xD983…1071 on V3, burn id 0x0b8e808d…4245471f (Transfer to the zero address, then BurnRequested) |
0x42d70ae9… |
Sepolia 11,955,414 | success, 159,354 gas |
| 48 | Committee member 1 funded on V3: 10 tPOT from the testnet account 5ERJ…gPJ7, enough to open a release |
0xe1c37a36…30209617 |
V3 #117,514 | final, fee 0.015 tPOT |
| 49 | Committee member 2 funded on V3: 1 tPOT, enough to run one | 0x1b073b16…b2f89b9d |
V3 #117,525 | final, fee 0.015 tPOT |
| 50 | Member 1 opens the release: multisig.asMulti without a timepoint, with the call in full (NewMultisig; 9.27 tPOT reserved until it runs) |
0xde2695cb…004e7765 |
V3 #117,688 | final, fee 0.033 tPOT |
| 51 | Member 2 runs it: asMulti at timepoint #117,688 → MultisigExecuted Ok, the vault's Released, 0.5 tPOT to 0xD983…1071; released[burnId] true, outstanding 1 → 0.5 tPOT |
0x686643de…14406011 |
V3 #117,701 | final, fee 0.034 tPOT |
| 56 | Burn 1 on Sepolia: 0.1 wtPOT burned for 0xD983…1071 on V3, burn id 0x1573ff21…f9219c0e (Transfer to the zero address, then BurnRequested) |
0x4e5f4c6e… |
Sepolia 11,983,026 | success, 61,434 gas |
| 57 | Fast mode, by the auto-release watcher: member 1 opens the release with the burn at least 3 blocks deep, before Sepolia finalized it: multisig.asMulti without a timepoint, with the call in full (NewMultisig; 9.27 tPOT reserved until it runs) |
0xd0c0cb48…3f9d2a74 |
V3 #173,057 | final, fee 0.033 tPOT |
| 58 | Member 2 runs it: asMulti at timepoint #173,057 → MultisigExecuted Ok, the vault's Released, 0.1 tPOT to 0xD983…1071; released[burnId] true, outstanding 1.5 → 1.4 tPOT; 84 seconds after the burn's block |
0xe238a6bd…539035e7 |
V3 #173,062 | final, fee 0.034 tPOT |
- The multisig paid each release's storage deposit, 3.8415 tPOT for
released[burnId]; member 1's 9.27 tPOT deposit came back when the release ran. - On the testnet one operator holds all three member keys, so, like the V3 validators' development keys, the committee shows the mechanics, not independent approvers.
- After row 51, the vault's
outstanding(0.5 tPOT) equaled the wtPOT supply (0.5 wtPOT), with no burn waiting for a release; after row 58, both are 1.4 (lock 1's mint is row 55).npm run release -- statuschecks that accounting. - Rows 56 to 58, Oct 24 (UTC): the standard way back (rows 47 to 51) waited for Sepolia's finality
and, released by hand, took 19 minutes 42 seconds from the burn's block (12:52:12) to the
release's (13:11:54); fast mode does not wait. The auto-release watcher, running since about
09:50 as
release --watch 15 --confirmations 3 --sendwith members 1 and 2's keys on the same machine as the relayer, read the burn at the block 3 below Sepolia's latest, ran every check of the release tool, and sent both approvals. The burn's Sepolia block is stamped 09:51:48 and the release's V3 block 09:53:12: 84 seconds. From starting the burn command (09:51:24) to the release's block, 108 seconds. Sepolia had not finalized the burn's block when the release ran. The official V3 testnet explorer shows the opening and the release. - Fast mode is still trusted (the committee), releases a burn of at most 1 tPOT (the watcher's cap), and keeps the fast releases whose burn is not final yet within 1 tPOT in all (its budget); a larger burn waits for Sepolia's finality. A reorg deeper than 3 blocks could undo a burn after its release (docs/BRIDGE.md).
Oct 20 (UTC), a timed run with the same tools, recorded for the demo video. The relayer first
brought the V3 light client to the validator set V3 had moved to (rows 52 and 53); then lock 1 was
sent on V3 and minted on Sepolia. The lock's V3 block is stamped 14:45:36 and the mint's Sepolia
block 14:46:12: 36 seconds. From sending the lock to the mint's receipt, with the tools' own checks,
it took 48 seconds: the lock 10, GRANDPA finality 14, reading and checking the proofs 9, the mint 15.
The bundle is fixtures/v3-testnet-lock1.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 52 | V3 testnet set change 32 → 33 at #118,123: 2 of 2 signatures | 0xc61b238b… |
Sepolia 11,955,971 | success, 2,390,572 gas |
| 53 | V3 testnet block #118,614 verified: the light client at the live head | 0xa420316f… |
Sepolia 11,955,972 | success, 1,877,730 gas |
| 54 | Lock 1 on V3: 1 tPOT locked for 0xD983…1071 on Sepolia; the vault stores the commitment 0x2d1ab009…4b0ad0a7 in commitments[1] |
0x30606cf1…8c6c70e4 |
V3 #118,636 | success, cost 3.864 tPOT, almost all storage deposit |
| 55 | Lock 1 minted: V3 block #118,637 verified (2 of 2, set 33), commitments[1] proven, 1 wtPOT to 0xD983…1071 |
0xb9134dd7… |
Sepolia 11,955,981 | success, 2,391,069 gas |
- Lock 1's mint did not bind the vault again (lock 0's mint did), so it used 2,391,069 gas, 14.3% of Sepolia's cap.
- After it, the vault's
outstanding(1.5 tPOT) equals the wtPOT supply (1.5 wtPOT).
Oct 27 (UTC), proof only: on two rollups that settle on Sepolia, the four libraries,
GrandpaLightClient #2 for the V3 testnet and ProvenBalance #2 bound to it, from the same build
as on Sepolia; no minter, no wtPOT and no old-mainnet client. The deployer was funded on each
through the rollup's canonical bridge from Sepolia (rows 59 and 60). Then
node contracts/script/deploy-sepolia.mjs --network base-sepolia|arbitrum-sepolia ran the plan on
the rollup's node with eth_simulateV1 and sent it: the libraries first, at the deployer's nonces
0 to 3, the addresses the build links them at; the client initialized at V3's validator set 59
from fixtures/v3-testnet-set59, with the set change at #211,277 that
started that set as its checkpoint; then V3 block #214,510 verified and an account balance proven
from its state root. Each step is recorded in deployments/base-sepolia.json
and deployments/arbitrum-sepolia.json, and the funding in
deployments/base-sepolia-funding.json and
deployments/arbitrum-sepolia-funding.json. The relayer
(npm run relay -- --network base-sepolia|arbitrum-sepolia --chain v3), with its key 0xE94a…0257
at that key's own nonces on each rollup, then imported V3's set change 59 → 60 and a newer head
(rows 79 to 82), recorded in
deployments/relay-base-sepolia-0xe94a….jsonl
and
deployments/relay-arbitrum-sepolia-0xe94a….jsonl.
| Contract (source-verified) | Base Sepolia | Arbitrum Sepolia |
|---|---|---|
GrandpaLightClient #2 (V3 testnet) |
0x3897…0ab6 |
0x3897…0ab6 |
ProvenBalance #2 |
0x40f4…86f0 |
0x40f4…86f0 |
Linked libraries, at the same addresses on both rollups as on Sepolia: SCL_EIP6565 0x51da…b77e,
GrandpaCodec 0x482a…dfd2, AuthoritySets 0xf2cd…e6d2, StorageProofs 0x5da0…f2a3.
All six have their source on Basescan and on Arbiscan, submitted on Oct 31 (UTC) by
node contracts/script/deploy-sepolia.mjs --network base-sepolia|arbitrum-sepolia --verify from the
deployment build, whose creation-code hashes are the ones each record holds. Etherscan's API reports
each one verified from its own submission, not as a Similar Match to Sepolia's, and the sources and
settings submitted compile to the input of each deployment transaction, byte for byte, its metadata
hash included.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 59 | 0.01 ETH bridged from Sepolia to the deployer on Base Sepolia: Base's L1StandardBridge.bridgeETH (ETHBridgeInitiated, then the OptimismPortal's TransactionDeposited) |
0x56aba453… |
Sepolia 12,004,572 | success, 635,305 gas |
| 60 | 0.01 ETH bridged from Sepolia to the deployer on Arbitrum Sepolia: the Delayed Inbox's depositEth (InboxMessageDelivered) |
0xb0d8e102… |
Sepolia 12,004,601 | success, 189,894 gas |
| 61 | Base Sepolia: SCL_EIP6565 created (ed25519; the deployer's nonce 0) |
0x4ef76f6f… |
Base 50,885,111 | success, 2,064,643 gas |
| 62 | Base Sepolia: GrandpaCodec created (nonce 1) |
0xdc095606… |
Base 50,885,141 | success, 1,433,476 gas |
| 63 | Base Sepolia: AuthoritySets created (nonce 2) |
0x199de82d… |
Base 50,885,170 | success, 1,277,278 gas |
| 64 | Base Sepolia: StorageProofs created (nonce 3) |
0x8be6efb6… |
Base 50,885,199 | success, 1,810,064 gas |
| 65 | Base Sepolia: GrandpaLightClient #2 created, for the V3 testnet |
0x84e4e5f5… |
Base 50,885,228 | success, 2,136,019 gas |
| 66 | Base Sepolia: ProvenBalance #2 created, bound to #2 |
0xcb9de232… |
Base 50,885,257 | success, 953,593 gas |
| 67 | Base Sepolia: V3 testnet client initialized at a checkpoint: set 59, its 2 keys, at the set change #211,277 that started it (the one trusted, admin step) | 0xdc3826c3… |
Base 50,885,286 | success, 502,786 gas |
| 68 | Base Sepolia: V3 testnet block #214,510 verified (2 of 2), and an account balance proven from its state root (System.Account, trie V1) | 0x195fb845… |
Base 50,885,313 | success, 2,113,075 gas |
| 69 | Arbitrum Sepolia: SCL_EIP6565 created (the deployer's nonce 0) |
0x91ce1a77… |
Arbitrum 323,329,901 | success, 2,064,643 gas |
| 70 | Arbitrum Sepolia: GrandpaCodec created (nonce 1) |
0xe6dfe170… |
Arbitrum 323,329,924 | success, 1,433,476 gas |
| 71 | Arbitrum Sepolia: AuthoritySets created (nonce 2) |
0xd7440fa1… |
Arbitrum 323,329,947 | success, 1,277,278 gas |
| 72 | Arbitrum Sepolia: StorageProofs created (nonce 3) |
0xdd751465… |
Arbitrum 323,329,971 | success, 1,810,064 gas |
| 73 | Arbitrum Sepolia: GrandpaLightClient #2 created, for the V3 testnet |
0xa52718f5… |
Arbitrum 323,329,995 | success, 2,136,019 gas |
| 74 | Arbitrum Sepolia: ProvenBalance #2 created, bound to #2 |
0x1490d644… |
Arbitrum 323,330,019 | success, 953,593 gas |
| 75 | Arbitrum Sepolia: V3 testnet client initialized at a checkpoint: set 59, its 2 keys, at the set change #211,277 that started it (the one trusted, admin step) | 0xefa2bd11… |
Arbitrum 323,330,044 | success, 502,786 gas |
| 76 | Arbitrum Sepolia: V3 testnet block #214,510 verified (2 of 2), and an account balance proven from its state root (System.Account, trie V1) | 0x26ca48f1… |
Arbitrum 323,330,067 | success, 2,113,075 gas |
| 77 | Base Sepolia: the relayer key 0xE94a…0257 funded with 0.008 ETH by the deployer (nonce 8) |
0x3835f070… |
Base 50,886,193 | success, 21,000 gas |
| 78 | Arbitrum Sepolia: the relayer key 0xE94a…0257 funded with 0.008 ETH by the deployer (nonce 8) |
0x3ca50316… |
Arbitrum 323,330,360 | success, 21,000 gas |
| 79 | Base Sepolia: V3 testnet set change 59 → 60 at #214,864: 2 of 2 signatures | 0x19944605… |
Base 50,886,825 | success, 1,849,818 gas |
| 80 | Base Sepolia: V3 testnet block #216,777 verified: the light client at the live head | 0xe7cddd90… |
Base 50,886,860 | success, 1,693,283 gas |
| 81 | Arbitrum Sepolia: V3 testnet set change 59 → 60 at #214,864: 2 of 2 signatures | 0xe4d14094… |
Arbitrum 323,331,271 | success, 1,849,818 gas |
| 82 | Arbitrum Sepolia: V3 testnet block #216,800 verified: the light client at the live head | 0xbb74d635… |
Arbitrum 323,331,302 | success, 1,693,974 gas |
- Same addresses, different roles: the deployer's nonces create the same addresses on every chain,
so the libraries sit where they do on Sepolia, but on Base Sepolia and Arbitrum Sepolia
0x3897…0ab6is the V3 testnet's client and0x40f4…86f0itsProvenBalance, while on Sepolia these two addresses are the old mainnet's (rows 1 to 4). Each network's addresses are in its record,deployments/<network>.json. - The checkpoint is the set change that started set 59, so the client starts in the set that signed the head and has no set change to import before it. No tampered proof is sent on the rollups; rows 4, 8 and 33 show those on Sepolia.
- Both clients hold V3 block #214,510's state root as its header on the V3 node has it,
0x66209f3b…55178eda. Each is as current as the relayer's last run there (see Known limits). - Cost: the 8 deployment transactions took 0.0000615 ETH on Base Sepolia and 0.000247 ETH on Arbitrum Sepolia, and the relayer's first two imports 0.0000177 and 0.0000710 ETH. The two deposits paid 0.0000000008 Sepolia ETH of gas.
Oct 27 (UTC). npm run automint (relayer/src/automint.ts) runs in
watch mode next to the relayer, from a key of its own, 0x57cc…A113, that holds only gas: mint
and importAndMint take nothing from msg.sender, so the key has no rights. Each round it reads
the vault's Locked events on V3 (by eth_getLogs, and in System.Events for a lock sent as a
Substrate extrinsic from a 5… account, whose event eth_getLogs does not return; see
docs/V3.md), waits for V3's finality and a 90-second grace (so that a user
minting from the page is not raced), checks that the lock names this chain and minter and is at
least 0.001 tPOT, and sends the cheapest mint that needs no waiting: mint at a V3 header the
light client already holds, or importAndMint at V3's finalized head. Before every send it
dry-runs the transaction and reads used(nonce) again; it writes each attempt, signed hash and
result to its history before and after sending, keeps to 0.02 ETH of gas a day, and refuses the
relayer's keys and the light clients' admin. Lock 2 was sent for 0xD983…1071; the mint came
from the auto-mint key, not from the lock's sender or recipient. The history is
deployments/automint-sepolia-0x57cc….jsonl.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 83 | The auto-mint key 0x57cc…A113 funded with 0.02 ETH by the deployer |
0x903cbf49… |
Sepolia 12,006,115 | success, 204,600 gas |
| 84 | Lock 2 on V3: 0.01 tPOT locked for 0xD983…1071 on Sepolia; the vault stores the commitment 0x7a4d0e3f…b7e78636 in commitments[2] |
0xed0435a7…eed84f79 |
V3 #219,589 | success, cost 3.864 tPOT, almost all storage deposit |
| 85 | Lock 2 minted by auto-mint: V3 block #219,607 verified (2 of 2, set 61), commitments[2] proven, 0.01 wtPOT to 0xD983…1071, sent and paid by 0x57cc…A113 |
0xb6164465… |
Sepolia 12,006,140 | success, 2,496,157 gas |
- The lock's V3 block is stamped 15:49:00 and the mint's Sepolia block 15:51:24: 144 seconds, 90 of them the grace. Auto-mint signed the mint at 15:51:13; it paid 1,024 wei per gas, about 0.0000000026 ETH.
- After it, the vault's
outstanding(1.41 tPOT) equals the wtPOT supply (1.41 wtPOT).
Oct 27 (UTC). Lock 3 is a revive.call signed by the testnet account 5ERJ…gPJ7 with
npm run v3-vault -- lock --send --key-file: the same kind of lock the app's Portaldot Wallet path
sends. The vault sees as msg.sender the account's address 0xB621…4e24, the last 20 bytes of the
keccak-256 of its account id. Its Locked event is a revive.ContractEmitted event in V3's
System.Events, which eth_getLogs does not return: V3's EVM RPC indexes the logs of Ethereum
transactions only. Auto-mint, restarted with the new lookup
(relayer/src/substrate-locks.ts), found the lock's block
through the vault's nextNonce, read the event there, checked it against commitments[3] and
minted it like lock 2; npm run mint and both status commands read such locks the same way.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 86 | Lock 3 on V3 from a Substrate account: revive.call signed by 5ERJ…gPJ7 (address 0xB621…4e24), 0.001 tPOT locked for 0xD983…1071 on Sepolia; the vault stores the commitment 0x423bce04…d7c052f0 in commitments[3]; its Locked event is in System.Events, not in eth_getLogs |
0x953fc65c…4627b7f7 |
V3 #222,024 | success, cost 3.867 tPOT, almost all storage deposit |
| 87 | Lock 3 minted by auto-mint, found in System.Events: mint(p) at V3 #222,687, which GrandpaLightClient #2 already held, commitments[3] proven, 0.001 wtPOT to 0xD983…1071, sent and paid by 0x57cc…A113 |
0xb6e2a07b… |
Sepolia 12,007,927 | success, 604,259 gas |
- Lock 3 was sent at 19:53 UTC and waited for the new lookup, which went live at 21:47; auto-mint minted it in its first round after the 90-second grace, so the gap is not a lock-to-mint time.
mint(p)at a header the client already holds imports nothing and checks no signature in the transaction: 604,259 gas, against 2,496,157 for lock 2'simportAndMint.- After it, the vault's
outstanding(1.411 tPOT) equals the wtPOT supply (1.411 wtPOT).
Oct 27 (UTC), the first stage of bridging to the two rollups: the same BridgeMinter and
WrappedTPOT as on Sepolia, from the same build (the creation-code hashes
deployments/sepolia-bridge.json records), bound to the same V3
vault and to each rollup's own V3 light client, 0x3897…0ab6. First the relayer
(npm run relay -- --network base-sepolia|arbitrum-sepolia --chain v3, one run on each, with its
key 0xE94a…0257 at that key's nonces 2 to 4 there) brought both clients from V3's set 60 to set
62 and a newer head, recorded after rows 79 to 82 in
deployments/relay-base-sepolia-0xe94a….jsonl
and
deployments/relay-arbitrum-sepolia-0xe94a….jsonl.
Then npm run mint -- deploy --network base-sepolia|arbitrum-sepolia ran the two creations on the
rollup's node with eth_simulateV1 and sent them, each hash recorded before it was published:
BridgeMinter at the deployer's nonce 9 and WrappedTPOT at 10, the same two addresses on both
rollups. The records are deployments/base-sepolia-bridge.json
and deployments/arbitrum-sepolia-bridge.json. No lock
names these minters yet: the first mint on each rollup, which also binds the vault, comes with
auto-mint there. Both contracts have their source on Basescan and on Arbiscan, submitted on Oct 31
(UTC) with the forge verify-contract command the mint tool prints for each, from the same build
and checked the same way as the light client's.
| Contract (source-verified) | Base Sepolia | Arbitrum Sepolia |
|---|---|---|
BridgeMinter |
0xdc31…2348 |
0xdc31…2348 |
WrappedTPOT (wtPOT, 14 decimals) |
0xE113…A95e |
0xE113…A95e |
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 88 | Base Sepolia: V3 testnet set change 60 → 61 at #218,444: 2 of 2 signatures | 0x4d5ced50… |
Base 51,062,089 | success, 1,843,410 gas |
| 89 | Base Sepolia: V3 testnet set change 61 → 62 at #222,029: 2 of 2 signatures | 0xbda80a22… |
Base 51,062,104 | success, 1,848,615 gas |
| 90 | Base Sepolia: V3 testnet block #222,589 verified: the light client at the live head | 0x3e5f5d54… |
Base 51,062,119 | success, 1,697,753 gas |
| 91 | Arbitrum Sepolia: V3 testnet set change 60 → 61 at #218,444: 2 of 2 signatures | 0xe217d7a7… |
Arbitrum 323,462,625 | success, 1,843,410 gas |
| 92 | Arbitrum Sepolia: V3 testnet set change 61 → 62 at #222,029: 2 of 2 signatures | 0x0c6efaa1… |
Arbitrum 323,462,653 | success, 1,848,615 gas |
| 93 | Arbitrum Sepolia: V3 testnet block #222,627 verified: the light client at the live head | 0x5f0d176c… |
Arbitrum 323,462,680 | success, 1,691,492 gas |
| 94 | Base Sepolia: BridgeMinter created (the deployer's nonce 9), taking the rollup's V3 client 0x3897…0ab6, the vault 0xa02E…8ffd on chain 420420777 and its code hash, and the token at nonce 10 |
0xdf59cc50… |
Base 51,063,602 | success, 1,399,559 gas |
| 95 | Base Sepolia: WrappedTPOT created (nonce 10), the minter its only minter |
0x0c6bbe45… |
Base 51,063,630 | success, 471,093 gas |
| 96 | Arbitrum Sepolia: BridgeMinter created (nonce 9), with the same arguments |
0x6ac2ed6e… |
Arbitrum 323,463,305 | success, 1,399,559 gas |
| 97 | Arbitrum Sepolia: WrappedTPOT created (nonce 10), the minter its only minter |
0xf219d762… |
Arbitrum 323,463,329 | success, 471,093 gas |
- Same addresses, one chain each: a minter builds every commitment from
block.chainidand its own address, so a lock names one chain and one minter. A lock for Base Sepolia does not mint on Arbitrum Sepolia, and one for Sepolia mints on neither;0xdc31…2348and0xE113…A95ehold no code on Sepolia. - Read back from both rollups, two RPCs each: the code at both addresses is the build's with the
constructor arguments as its immutables,
lightClient()is that rollup's V3 client (genesis0xd4f9acbc…155283bd, set 62), and the token's minter is the minter, with 14 decimals and no supply. Neither minter is bound yet; a mint before the bind revertsVaultNotBound(). - The same creations used 9,806,643 and 3,183,649 gas on Sepolia (rows 24 and 25), and 1,399,559 and 471,093 on both rollups.
- Cost: the two creations took 0.0000094 ETH on Base Sepolia and 0.0000376 ETH on Arbitrum Sepolia; the relayer's three imports took 0.0000270 and 0.000108 ETH.
Oct 28 (UTC), the second stage: the tools for a lock that names a rollup's minter. npm run mint
(bundle, plan, mint and status) and npm run automint take --network base-sepolia or
arbitrum-sepolia, with that rollup's records (deployments/<network>-bridge.json), light client,
minter, RPCs and gas rules; auto-mint takes several, comma separated, and serves them from one
process, a round for one network after the other, each with its own state, history and locks
(automint-<network>-<key>.*). Without --network both tools work as before, on Sepolia.
- The first mint binds the vault. Neither rollup's minter is bound yet, and a mint before the
bind reverts
VaultNotBound(). The first mint therefore goes by the head path:importAndMintat V3's finalized head carries the vault's account proof and binds it in the same transaction, as lock 0's mint did on Sepolia (row 32).mint(p)at a header the client holds cannot bind, so until then a lock waits for the head path, which needs the client at V3's validator set. - The L1 part of the gas. On Arbitrum a transaction pays for its data on L1 in L2 gas units:
eth_estimateGasincludes them, and both tools ask NodeInterface'sgasEstimateL1Componentfor them and add them once more to the gas limit, as the L1 price can rise before inclusion. On Base the L1 data fee is charged besides the gas: GasPriceOracle'sgetL1FeeUpperBoundcounts in the balance a send needs and in auto-mint's--max-spend, and each mined transaction'sl1Feein what it spent. On Oct 28 Arbitrum Sepolia's L1 gas was 0 (its L1 base fee estimate was 0) and Base Sepolia's L1 fee under 0.000000001 ETH. - A lock that cannot be paid for yet waits without work. When a rollup's send waits for what it
costs (the auto-mint key's balance short,
--max-spendreached, or the base fee above--max-base-fee), auto-mint does not plan that lock again for 5 minutes, or until the key's balance rises or the base fee is back within the gate: each plan captures a bundle from the V3 node only to wait again, and one process serves every network. Sepolia's locks are planned every round, as before.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 98 | Lock 4 on V3: 0.001 tPOT locked for 0xD983…1071 on Base Sepolia (chain 84532, minter 0xdc31…2348), from the deployer's EVM account; the vault stores the commitment 0x91148935…6c514f61 in commitments[4] |
0x8e26f3ed…2f669bc5 |
V3 #224,435 | success, cost 3.864 tPOT, almost all storage deposit |
| 99 | Lock 5 on V3: 0.001 tPOT locked for 0xD983…1071 on Arbitrum Sepolia (chain 421614, the same minter address); the vault stores the commitment 0x636ef291…e540beb9 in commitments[5] |
0x22d2ecbe…f4b05c21 |
V3 #224,440 | success, cost 3.864 tPOT, almost all storage deposit |
- The dry runs, nothing signed: auto-mint for both rollups in one process found lock 4 for Base
Sepolia and lock 5 for Arbitrum Sepolia, skipped the other rollup's lock and Sepolia's locks 0 to
3, and planned each first mint:
importAndMintat V3's finalized head, signed by set 62, with the vault's account proof (9 items).eth_callandeth_estimateGaspassed: 2,661,282 gas on Base Sepolia (limit 2,927,411, and an L1 data fee of at most 0.0000000008 ETH) and 2,629,877 on Arbitrum Sepolia (L1 gas 0; limit 2,892,865).npm run mint -- plan --network base-sepolia --nonce 4and--network arbitrum-sepolia --nonce 5ran the same mints witheth_simulateV1:HeaderImported,VaultBound,MintedandTransfer, 2,639,946 and 2,612,827 gas, and the same call again revertingNonceAlreadyUsed. - Sepolia's auto-mint saw both locks and recorded them as skipped, for another chain, as it does every lock that names another minter.
- After a lock, the rollup's client has to be at V3's validator set for the first mint, as for any
mint by the head path. The rollups' clients are kept there by relayer runs on a timer, with the
relayer key
0xE94a…0257at its own nonces on each rollup: one run per rollup after each V3 set change (V3 changed sets every 6 hours, at 01:54, 07:54, 13:54 and 19:54 UTC, until Oct 28), and since Oct 31 a run every 20 minutes, as the times moved (Known limits).
Then the same morning (UTC), the first mints. The deployer funded the auto-mint key 0x57cc…A113 on
each rollup from what it held there, the relayer brought both clients from V3's set 62 to set 63,
and one auto-mint process, started with
--network sepolia,base-sepolia,arbitrum-sepolia, minted lock 4 on Base Sepolia and lock 5 on
Arbitrum Sepolia, each with the bind: both rollups' minters now take the vault's trie id
0xbe307c2d…, the same as Sepolia's (row 32). The histories are
deployments/automint-base-sepolia-0x57cc….jsonl
and
deployments/automint-arbitrum-sepolia-0x57cc….jsonl.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 100 | Base Sepolia: the auto-mint key 0x57cc…A113 funded with 0.0015 ETH by the deployer (nonce 11) |
0x865ca57b… |
Base 51,158,146 | success, 21,000 gas |
| 101 | Arbitrum Sepolia: the auto-mint key 0x57cc…A113 funded with 0.0012 ETH by the deployer (nonce 11) |
0xb5c86bda… |
Arbitrum 323,533,471 | success, 21,000 gas |
| 102 | Base Sepolia: V3 testnet set change 62 → 63 at #225,621: 2 of 2 signatures | 0x4aec9d9a… |
Base 51,158,482 | success, 1,846,781 gas |
| 103 | Base Sepolia: V3 testnet block #225,798 verified: the light client at the live head | 0x17776e09… |
Base 51,158,516 | success, 1,692,678 gas |
| 104 | Arbitrum Sepolia: V3 testnet set change 62 → 63 at #225,621: 2 of 2 signatures | 0x0f783aeb… |
Arbitrum 323,533,962 | success, 1,846,781 gas |
| 105 | Arbitrum Sepolia: V3 testnet block #225,808 verified: the light client at the live head | 0xf89626e0… |
Arbitrum 323,533,993 | success, 1,692,937 gas |
| 106 | Lock 4 minted on Base Sepolia by auto-mint: importAndMint at V3 block #225,845 (2 of 2, set 63), the vault bound, commitments[4] proven, 0.001 wtPOT to 0xD983…1071, sent and paid by 0x57cc…A113 |
0x0f300c8e… |
Base 51,159,736 | success, 2,637,646 gas |
| 107 | Lock 5 minted on Arbitrum Sepolia by auto-mint: importAndMint at V3 block #225,852 (2 of 2, set 63), the vault bound, commitments[5] proven, 0.001 wtPOT to 0xD983…1071, sent and paid by 0x57cc…A113 |
0x4443486b… |
Arbitrum 323,534,868 | success, 2,605,555 gas |
- One vault on Portaldot now backs wtPOT on three chains: each lock names one chain and one minter,
and each chain's minter proves it against that chain's own V3 light client. After rows 106 and
107 the wtPOT supply is 0.001 on Base Sepolia and 0.001 on Arbitrum Sepolia, and 1.411 on
Sepolia; the vault's
outstandingis 1.413 tPOT, their sum. - The way back from the rollups (a burn there, then the committee's release on V3) followed the same day (below).
Oct 28 (UTC), the third stage: the trusted way back from the rollups. npm run release
(relayer/src/release.ts; burn, release and status) takes
--network base-sepolia or arbitrum-sepolia, with that rollup's records
(deployments/<network>-bridge.json, where a burn is a step of kind burn, as on Sepolia), RPCs and
rule (relayer/src/wayback.ts); the auto-release watcher,
release --watch, takes several, comma separated, and serves them from one process, a round for one
chain after the other. Without --network both work as before, on Sepolia; the watcher then warns
that the rollups' burns wait, and with --send does not start while the record holds a rollup's
release that only a round of that rollup finishes (one left open, or one made fast and not checked
yet). On V3 nothing changes:
the committee's 2-of-3 multisig has the vault release the burn's amount to its v3Recipient, once
per burn id, and nothing on V3 checks the burn.
- Burn ids differ per chain. Both rollups' minters are at
0xdc31…2348and count their burns from 0;burnIdOf(n)putsblock.chainidin the id, so burn 0 on Base Sepolia is0x6ae40d53…bedb6d8dand burn 0 on Arbitrum Sepolia0x9890de41…cfe80522. The vault'sreleased[burnId]pays each once; the watcher keeps each chain's releases apart indeployments/v3-testnet.jsonby the burn's chain id and minter, and its burn cache per network. - Final, on a rollup, is its
finalizedblock tag, which follows Sepolia's finality of the batch that holds the burn: an OP Stack node (Base) derives it from the batches in Sepolia's finalized blocks, an Arbitrum node marks a block finalized once its batch is in a finalized Sepolia block. On Oct 28 that was about 20 minutes after a block: Base Sepolia's finalized block was 5,800 to 6,700 of its 0.2 s blocks below its latest, Arbitrum Sepolia's 3,800 to 4,500 of its 0.25 s blocks, against about 80 of Sepolia's 12 s blocks. - Fast mode, on a rollup, reads a burn
--confirmationsof that chain's blocks below its latest, up to the fast cap; with--sendat least 180 on Base Sepolia and 144 on Arbitrum Sepolia, the 36 seconds Sepolia's 3 take. Until its batch is on Sepolia a rollup's sequencer alone orders its latest blocks: a restart or a failover that drops them is the reorg that could undo a fast burn there.--confirmationstakes one number for every chain, or<network>=<n>pairs, a chain left out waiting for finality. - One fast budget. One vault and one committee release for every chain, so the fast releases
whose burns are not final on their own chain yet (with
--send, or not checked against it yet) stay within--fast-budget, 1 tPOT by default, across the three chains; an approval by hand in fast mode counts them too. The committee's lock and halt file are shared too: one watcher with--sendserves every chain, and--rounds nruns n rounds of each. - A burn on a rollup. Its dry run's first pass runs at 1,000,000 gas, as the cap at the max fee
would need more ETH than a burner holds there, and it is sent with
eth_estimateGas's + 10%: a burn of the sender's whole balance clears two storage slots, whose refund leaves the gas used (52,822) below what the burn needs to run (62,517). Arbitrum's L1 gas is added once more to the limit and Base's L1 data fee counted in the balance, as for a mint, and the record keeps the receipt'sl1FeeorgasUsedForL1. A read of state that Arbitrum Sepolia's own RPC no longer serves (it keeps about 4,500 blocks, about as deep as its finalized block) goes to an archive RPC.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 108 | Base Sepolia: burn 0, 0.001 wtPOT burned for 0xD983…1071 on V3, burn id 0x6ae40d53…bedb6d8d (Transfer to the zero address, then BurnRequested), from the deployer (nonce 12) |
0xaffaf9e0…70eefd2e |
Base 51,177,673 | success, 52,822 gas |
| 109 | Arbitrum Sepolia: burn 0, 0.001 wtPOT burned for 0xD983…1071 on V3, burn id 0x9890de41…cfe80522 (Transfer to the zero address, then BurnRequested), from the deployer (nonce 12) |
0xc2ec4b93…81f48cb8 |
Arbitrum 323,549,062 | success, 52,822 gas |
| 110 | Base Sepolia's burn 0, by the auto-release watcher: member 1 opens the release with the burn final on Base Sepolia: multisig.asMulti without a timepoint, with the call in full (NewMultisig; 9.27 tPOT reserved until it runs) |
0x0e99ca5b…b42a22eb |
V3 #227,761 | final, fee 0.033 tPOT |
| 111 | Member 2 runs it: asMulti at timepoint #227,761 → MultisigExecuted Ok, the vault's Released, 0.001 tPOT to 0xD983…1071; released[burnId] true, outstanding 1.413 → 1.412 tPOT |
0x0bfe3286…5b59b52f |
V3 #227,765 | final, fee 0.034 tPOT |
| 112 | Arbitrum Sepolia's burn 0, by the auto-release watcher: member 1 opens the release with the burn final on Arbitrum Sepolia: multisig.asMulti without a timepoint, with the call in full (NewMultisig; 9.27 tPOT reserved until it runs) |
0x48221e33…a5b20197 |
V3 #227,772 | final, fee 0.033 tPOT |
| 113 | Member 2 runs it: asMulti at timepoint #227,772 → MultisigExecuted Ok, the vault's Released, 0.001 tPOT to 0xD983…1071; released[burnId] true, outstanding 1.412 → 1.411 tPOT |
0x9ed92cde…97910c9f |
V3 #227,777 | final, fee 0.034 tPOT |
- Sent with
npm run release -- burn --network base-sepolia|arbitrum-sepolia --amount 0.001 --v3-recipient 0xD9838A86cF83C28Ee486544060D1cA4Ca2971071 --send --account bz-deployer --password-file <path>, each hash recorded indeployments/<network>-bridge.jsonbefore it was published. Each burned the deployer's whole 0.001 wtPOT there: the wtPOT supply is 0 on both rollups, and until the releases the vault'soutstanding, 1.413 tPOT, covered Sepolia's 1.411 wtPOT and these two burns. Base Sepolia's burn paid an L1 data fee of 666,738 wei besides its gas; Arbitrum Sepolia's L1 gas was 0. - The dry runs, nothing signed:
npm run release -- release --burn-nonce 0 --network base-sepolia|arbitrum-sepolia, in fast mode (--confirmations 180and144) a minute after the burns, and at each chain's finalized block once it had passed them, about 18 minutes after the burns (Base Sepolia's finalized #51,177,966, Arbitrum Sepolia's #323,549,592); then the watcher for both rollups in one process (release --watch 15 --rounds 1 --network base-sepolia,arbitrum-sepolia, in fast mode and at finality), and for the three chains as it is to run (--network sepolia,base-sepolia,arbitrum-sepolia --confirmations sepolia=3,base-sepolia=180,arbitrum-sepolia=144). Each found the burn on its chain, checked it, and dry-ran its release from the committee's multisig on V3 (reviveApi.calland its trace: theReleasedlog and the payout of 0.001 tPOT to0xD983…1071, storage deposit 3.8415 tPOT; call hashes0xe2ce3541…fe0f25f1and0xf3c92150…77a13855), then member 1's openingasMulti. One watcher round on Base Sepolia waited instead: a backend of its public RPC still answered an older finalized block, and the next round found the burn final. - The releases, rows 110 to 113: the auto-release watcher, started at 05:28 UTC with this code (the
record names git
9fa74ae) asrelease --watch 15 --network sepolia,base-sepolia,arbitrum-sepolia --confirmations sepolia=3,base-sepolia=180,arbitrum-sepolia=144 --send, with members 1 and 2's keys, found each burn final on its rollup ("by": "finality"in the record, not fast mode), ran every check of the release tool and sent both approvals, Base Sepolia's release signed at 05:29 UTC and Arbitrum Sepolia's at 05:30, by the record. The burns were sent at 03:16 and 03:17 and waited for this watcher, so that gap is not a burn-to-release time. The command that starts the watcher again at logon has to run the same line (docs/BRIDGE.md). - The multisig paid each release's storage deposit, 3.8415 tPOT for
released[burnId]; member 1's 9.27 tPOT deposit came back when each release ran. After row 113, the vault'soutstanding(1.411 tPOT) equals the wtPOT supply on the three chains (1.411 wtPOT, all of it on Sepolia), with no burn waiting for a release: 0.602 tPOT released on V3 in all, 0.5 and 0.1 for Sepolia's burns and 0.001 for each rollup's.
Oct 28 (UTC), the first transfers through the bridge app (on the branch app, run locally, not yet
published) from a wallet outside the bridge's own keys: the owner's own MetaMask wallet,
0xc6e9…2011. The deployer funded it on the three chains (rows 114 to 116). In the app the wallet
then signed four transactions, the two locks on V3 and the two burns on the rollups; auto-mint
minted each lock on the chain it names (rows 117 to 120), and the auto-release watcher released
each burn on V3 to the same wallet, in fast mode (rows 121 to 126). The V3 record does not hold
these locks, which the app sent: auto-mint's histories
deployments/automint-base-sepolia-0x57cc….jsonl
and
deployments/automint-arbitrum-sepolia-0x57cc….jsonl
hold their mints, and deployments/v3-testnet.json the releases,
each with its burn. The funding and the two locks on V3 (rows 114, 117 and 119), which no tool
recorded when they were sent, are in deployments/v3-app-transfers.json,
read back from V3's EVM RPC: each one's full hash, sender, nonce and block.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 114 | The owner's wallet 0xc6e9…2011 funded on V3: 12 tPOT from the deployer's EVM account 0xD983…1071 (nonce 5), enough for two locks of 1 tPOT and their costs |
0x6b06421f…dd04ba45 |
V3 #231,112 | success, cost 0.113 tPOT, 0.1 of it the new account's existential deposit |
| 115 | Base Sepolia: the owner's wallet 0xc6e9…2011 funded with 0.0001 ETH by the deployer (nonce 13) |
0x3c891df3… |
Base 51,318,352 | success, 21,000 gas |
| 116 | Arbitrum Sepolia: the owner's wallet 0xc6e9…2011 funded with 0.0001 ETH by the deployer (nonce 13) |
0xbfa5d236… |
Arbitrum 323,659,996 | success, 21,000 gas |
| 117 | Lock 6 on V3: 1 tPOT locked for 0xc6e9…2011 on Base Sepolia (chain 84532, minter 0xdc31…2348), from the same wallet in the app (its nonce 0); the vault stores the commitment 0x37ab67ca…d2531f1d in commitments[6] |
0x6b31b8a9…186d5455 |
V3 #231,125 | success, cost 3.864 tPOT, almost all storage deposit |
| 118 | Lock 6 minted on Base Sepolia by auto-mint: importAndMint at V3 block #231,144 (2 of 2, set 64), commitments[6] proven, 1 wtPOT to 0xc6e9…2011, sent and paid by 0x57cc…A113 |
0x7d9e535d… |
Base 51,319,421 | success, 2,249,855 gas |
| 119 | Lock 7 on V3: 1 tPOT locked for 0xc6e9…2011 on Arbitrum Sepolia (chain 421614, the same minter address), from the same wallet in the app (nonce 1); the vault stores the commitment 0xbe308b6f…4803061a in commitments[7] |
0xd125ef44…4277224b |
V3 #231,170 | success, cost 3.864 tPOT, almost all storage deposit |
| 120 | Lock 7 minted on Arbitrum Sepolia by auto-mint: importAndMint at V3 block #231,188 (2 of 2, set 64), commitments[7] proven, 1 wtPOT to 0xc6e9…2011, sent and paid by 0x57cc…A113 |
0xb351c94b… |
Arbitrum 323,661,852 | success, 2,253,034 gas |
| 121 | Arbitrum Sepolia: burn 1, 1 wtPOT burned for 0xc6e9…2011 on V3, burn id 0x4cc24e88…a133861e (Transfer to the zero address, then BurnRequested), from the same wallet in the app (its nonce 0 there) |
0x99eb6d4a…d6132e76 |
Arbitrum 323,663,335 | success, 36,268 gas |
| 122 | Arbitrum Sepolia's burn 1, by the auto-release watcher in fast mode: member 1 opens the release with the burn at least 144 blocks deep, before Arbitrum Sepolia finalized it: multisig.asMulti without a timepoint, with the call in full (NewMultisig; 9.27 tPOT reserved until it runs) |
0xf81b5402…29116f74 |
V3 #231,268 | final, fee 0.033 tPOT |
| 123 | Member 2 runs it: asMulti at timepoint #231,268 → MultisigExecuted Ok, the vault's Released, 1 tPOT to 0xc6e9…2011; released[burnId] true, outstanding 3.411 → 2.411 tPOT; 105 seconds after the burn's block |
0x7edcc840…c73e6cff |
V3 #231,273 | final, fee 0.034 tPOT |
| 124 | Base Sepolia: burn 1, 1 wtPOT burned for 0xc6e9…2011 on V3, burn id 0x4f3ae47f…674f689d (Transfer to the zero address, then BurnRequested), from the same wallet in the app (its nonce 0 there) |
0x7a36d5d3…95cf6ad9 |
Base 51,324,284 | success, 36,268 gas |
| 125 | Base Sepolia's burn 1, by the auto-release watcher in fast mode, once the fast budget had room again: member 1 opens the release with the burn at least 180 blocks deep, before Base Sepolia finalized it: multisig.asMulti without a timepoint, with the call in full (NewMultisig; 9.27 tPOT reserved until it runs) |
0x0e6680f1…d1873ab7 |
V3 #231,457 | final, fee 0.033 tPOT |
| 126 | Member 2 runs it: asMulti at timepoint #231,457 → MultisigExecuted Ok, the vault's Released, 1 tPOT to 0xc6e9…2011; released[burnId] true, outstanding 2.411 → 1.411 tPOT; 905 seconds after the burn's block |
0xcc9209a0…abf90ca0 |
V3 #231,461 | final, fee 0.034 tPOT |
- Lock to mint, by the block timestamps: lock 6's V3 block is stamped 11:06:54 and its mint's Base
Sepolia block 11:09:13, 139 seconds; lock 7's V3 block 11:11:24 and its mint's Arbitrum Sepolia
block 11:13:37, 133 seconds. 90 seconds of each are auto-mint's grace after V3's finality, so that
a mint from the app is not raced. Both mints took the head path,
importAndMintat V3's finalized head: each rollup's client was at V3's set 64, its latest header below the lock's block. - Burn to release, from Arbitrum Sepolia: the burn's block is stamped 11:19:57 and its release's V3
block 11:21:42, 105 seconds. The watcher took the burn in fast mode, with at least 144 of Arbitrum
Sepolia's blocks on top of it, instead of waiting for that chain's finality, against which it
checked the burn at 11:39:34, 19 minutes 37 seconds after the burn's block (below);
member 1 signed the opening at 11:21:11 and member 2 the run at 11:21:40, by the record. Both
releases were sent by the watcher that released rows 110 to 113, still running the same line;
their records name git
8995a94, a later commit with the same relayer code. - From Base Sepolia it took 905 seconds, the burn's block stamped 11:25:25 and its release's V3 block 11:40:30, because the fast budget was full. The fast releases whose burns are not final on their own chain yet stay within 1 tPOT in all, across the three chains, and row 123's 1 tPOT held that budget until the watcher had checked Arbitrum Sepolia's burn against that chain's finalized block (#323,664,378, at 11:39:34 by the record). Until then each round on Base Sepolia logged that the burn waits: the burns released fast and not final yet hold 1 tPOT, and with its own 1 tPOT that is above the budget. The next round, at 11:39:42, found it 4,336 blocks deep and released it, still in fast mode, before Base Sepolia finalized it (the watcher checked it against Base Sepolia's finalized block #51,324,490 at 11:43:16, by the record). That wait is the safety rule working: at no time did more than 1 tPOT of releases rest on burns that were not final on their chain.
- What the wallet paid: on V3, 3.864 tPOT for each lock, 3.8415 of it the storage deposit of the
vault's new
commitmentsentry and 0.023 the fee, 7.728 tPOT for the two; the 2 tPOT it locked came back with the releases. On the rollups, only its burns: 0.000000218 ETH on Base Sepolia (36,268 gas, and an L1 data fee of 92 wei) and 0.000000725 ETH on Arbitrum Sepolia (36,268 gas; L1 gas 0). Auto-mint's key paid the mints, 0.0000113 ETH on Base Sepolia and 0.0000451 ETH on Arbitrum Sepolia; members 1 and 2 paid the releases' fees, and the multisig each release's storage deposit, 3.8415 tPOT forreleased[burnId]; member 1's 9.27 tPOT deposit came back when each release ran. - After row 126 the wallet holds 4.2716 tPOT on V3 (its
eth_getBalance: the 12 less the two locks' costs, without the 0.1 tPOT existential deposit that row 114 paid), no wtPOT, and 0.0000998 ETH on Base Sepolia and 0.0000993 ETH on Arbitrum Sepolia. The vault'soutstanding(1.411 tPOT) equals the wtPOT supply on the three chains (1.411 wtPOT, all of it on Sepolia; 0 on both rollups), with no burn waiting for a release: 4.013 tPOT locked in all (locks 0 to 7), less 2.602 released on V3, 0.5 and 0.1 for Sepolia's burns, 0.001 for each rollup's burn 0 and 1 for each rollup's burn 1.
Oct 31 (UTC), the proof kit's contracts (For builders,
docs/PROOF-KIT.md), from the deployer
0xD983…1071
at its nonces 87 and 88. node contracts/script/deploy-kit.mjs first had a Sepolia node run both
creations with eth_simulateV1 and, in the same simulated block, five reads through the new Reader
from fixtures/v3-testnet-kit, each of which had to return the
fixture's value; then it sent them, each hash recorded before it was published, and checked each
receipt and the code at each address. The record is
deployments/sepolia-kit.json. Every read is against V3 #264,729,
the set change 73 → 74 that GrandpaLightClient #2 imported on Oct 31 with both validators'
signatures. Both contracts have their source on Etherscan as an Exact Match. No claim was sent to
the gate: no account of the project holds a V3 asset, and a claim names its holder, so the gate's
claims are shown by its forge tests.
| Contract (Sepolia, source-verified) | Address |
|---|---|
PortaldotStateReader |
0x2ea6…1fA9 |
ProvenAssetGate (asset 7777, at least 1 pUSD, at V3 #264,729) |
0x24aB…8d23 |
Sepolia blocks 12,034,957 and 12,034,965, Oct 31 (UTC):
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 127 | PortaldotStateReader created (the deployer's nonce 87), bound to GrandpaLightClient #2 and linked to StorageProofs |
0x28908bfb… |
Sepolia 12,034,957 | success, 12,050,460 gas |
| 128 | ProvenAssetGate created (nonce 88): at least 1 pUSD (asset 7777) at V3 #264,729, the block its constructor found in the light client through the Reader |
0x9a462c6f… |
Sepolia 12,034,965 | success, 2,989,057 gas |
- After both were mined, the Reader made the same five reads by
eth_callat Sepolia block 12,034,965, each returning the fixture's value, witheth_estimateGas's gas for the whole transaction: the deployer'sSystem.Account(nonce 6, 159.47352201127852 tPOT free), 452,621;0x3e79…dcdf'sAssets.Accountof pUSD (60.5, Liquid), 480,180; the owner of item 3,163,806,249 in NFT collection 10, 363,589; the vault'sRevive.AccountInfoOfentry (its trie id and code hash), 404,545; and the vault'scommitments[1], lock 1's commitment, with the vault's code hash, 877,450. - Creating the Reader took 71.8% of the 16,777,216 cap. Both creations cost the deployer 0.0000000152 ETH, at 1,013 wei per gas.
Nov 1 (UTC), the fourth destination, and the first that is not a rollup of Sepolia: the BNB Smart
Chain testnet (chain 97). Its validators finalize a block once their votes justify it and its child
(fast finality, BEP-126), about a second after it, and include no transaction below a priority fee
of 0.1 gwei; its base fee is 0. The same contracts as on the rollups, from the same build and at
the same addresses: the deployer, funded with tBNB from 0xBB06…dE9 (row 129), sent them at its
nonces 0 to 11 in the rollups' order, node contracts/script/deploy-sepolia.mjs --network bsc-testnet the four libraries, GrandpaLightClient #2 and ProvenBalance #2, and npm run mint -- deploy --network bsc-testnet BridgeMinter and wtPOT, each plan run first on the chain's node
with eth_simulateV1. The client was initialized at V3's validator set 75, with the set change
#266,908 that started it as its checkpoint
(fixtures/v3-testnet-set75), then verified V3 block #269,340 and an
account balance proven from its state root. The records are
deployments/bsc-testnet.json,
deployments/bsc-testnet-bridge.json and
deployments/bsc-testnet-funding.json, and
ops/bnb-activation.md is the runbook the steps followed.
| Contract (source-verified) | BNB Smart Chain testnet |
|---|---|
GrandpaLightClient #2 (V3 testnet) |
0x3897…0ab6 |
ProvenBalance #2 |
0x40f4…86f0 |
BridgeMinter |
0xdc31…2348 |
WrappedTPOT (wtPOT, 14 decimals) |
0xE113…A95e |
Linked libraries, at the same addresses as on Sepolia and the rollups: SCL_EIP6565 0x51da…b77e,
GrandpaCodec 0x482a…dfd2, AuthoritySets 0xf2cd…e6d2, StorageProofs 0x5da0…f2a3. Their
code is the build's, checked by both tools against each record's creation-code hash.
All eight have their source on BscScan, submitted on Nov 1 (UTC) through Etherscan's API by
node contracts/script/deploy-sepolia.mjs --network bsc-testnet --verify for the six of the light
client's deployment and by the forge verify-contract command the mint tool prints for
BridgeMinter and wtPOT, from the deployment build. Each one is verified from its own submission,
with the libraries the record names, and the sources stored there are the repository's files.
| # | What it shows | Transaction | Block | Result |
|---|---|---|---|---|
| 129 | The BNB Smart Chain testnet: 0.2999979 tBNB to the deployer from 0xBB06…dE9, which paid the gas of the rows that follow |
0x148259c0… |
BSC 140,152,968 | success, 21,000 gas |
| 130 | BSC testnet: SCL_EIP6565 created (ed25519; the deployer's nonce 0) |
0x656ff9c9… |
BSC 140,161,293 | success, 2,064,643 gas |
| 131 | BSC testnet: GrandpaCodec created (nonce 1) |
0x6a03ef2d… |
BSC 140,161,320 | success, 1,433,476 gas |
| 132 | BSC testnet: AuthoritySets created (nonce 2) |
0x78039c38… |
BSC 140,161,337 | success, 1,277,278 gas |
| 133 | BSC testnet: StorageProofs created (nonce 3) |
0xd523cf2f… |
BSC 140,161,356 | success, 1,810,064 gas |
| 134 | BSC testnet: GrandpaLightClient #2 created, for the V3 testnet (nonce 4) |
0x28013678… |
BSC 140,161,377 | success, 2,136,019 gas |
| 135 | BSC testnet: ProvenBalance #2 created, bound to #2 (nonce 5) |
0x6739f711… |
BSC 140,161,403 | success, 953,593 gas |
| 136 | BSC testnet: V3 testnet client initialized at a checkpoint: set 75, its 2 keys, at the set change #266,908 that started it (the one trusted, admin step) | 0xc6e6e0b8… |
BSC 140,161,424 | success, 504,776 gas |
| 137 | BSC testnet: V3 testnet block #269,340 verified (2 of 2), and an account balance proven from its state root (System.Account, trie V1) | 0xfb31780a… |
BSC 140,161,448 | success, 2,118,725 gas |
| 138 | BSC testnet: the relayer key 0xE94a…0257 funded with 0.1 tBNB by the deployer (nonce 8) |
0x1d9717e8… |
BSC 140,161,541 | success, 21,000 gas |
| 139 | BSC testnet: BridgeMinter created (nonce 9), taking the chain's V3 client 0x3897…0ab6, the vault 0xa02E…8ffd on chain 420420777 and its code hash, and the token at nonce 10 |
0x1136bbfe… |
BSC 140,162,374 | success, 1,399,559 gas |
| 140 | BSC testnet: WrappedTPOT created (nonce 10), the minter its only minter |
0x5b6add74… |
BSC 140,162,400 | success, 471,093 gas |
| 141 | BSC testnet: the auto-mint key 0x57cc…A113 funded with 0.05 tBNB by the deployer (nonce 11) |
0x64be203f… |
BSC 140,162,585 | success, 21,000 gas |
| 142 | BSC testnet: V3 testnet set change 75 → 76 at #270,477: 2 of 2 signatures | 0x58a7b4ed… |
BSC 140,164,852 | success, 1,852,257 gas |
| 143 | BSC testnet: V3 testnet set change 76 → 77 at #274,059: 2 of 2 signatures | 0xcfe9a8ae… |
BSC 140,164,870 | success, 1,844,978 gas |
| 144 | BSC testnet: V3 testnet block #275,530 verified: the light client at the live head | 0xa707d519… |
BSC 140,164,885 | success, 1,692,097 gas |
| 145 | Lock 8 on V3: 0.001 tPOT locked for 0xD983…1071 on the BNB Smart Chain testnet (chain 97, minter 0xdc31…2348), from the deployer's EVM account; the vault stores the commitment 0xde3757fd…dc59f93c in commitments[8] |
0x354cda49…d9e461ec |
V3 #275,563 | success, cost 3.864 tPOT, almost all storage deposit |
| 146 | Lock 8 minted on the BNB Smart Chain testnet by auto-mint: importAndMint at V3 block #275,580 (2 of 2, set 77), the vault bound, commitments[8] proven, 0.001 wtPOT to 0xD983…1071, sent and paid by 0x57cc…A113 |
0x81f73eb9… |
BSC 140,165,449 | success, 2,658,627 gas |
| 147 | BSC testnet: burn 0, 0.001 wtPOT burned for 0xD983…1071 on V3, burn id 0xf38d1b6c…50454773 (Transfer to the zero address, then BurnRequested), from the deployer (nonce 12) |
0x0a904a75…f61a92e3 |
BSC 140,165,763 | success, 52,822 gas |
| 148 | The BNB Smart Chain testnet's burn 0, by the auto-release watcher: member 1 opens the release with the burn final there: multisig.asMulti without a timepoint, with the call in full (NewMultisig; 9.27 tPOT reserved until it runs) |
0xc2088912…e0318461 |
V3 #275,611 | final, fee 0.033 tPOT |
| 149 | Member 2 runs it: asMulti at timepoint #275,611 → MultisigExecuted Ok, the vault's Released, 0.001 tPOT to 0xD983…1071; released[burnId] true, outstanding 1.412 → 1.411 tPOT |
0x4c715149…f7ed58c8 |
V3 #275,615 | final, fee 0.034 tPOT |
- The server ran it from the start. Before the first lock, the server's standing code update
put the chain in its units: the release watcher and auto-mint serve four chains, and the
rollups' 20-minute relay runs take a third run, for chain 97. The relayer's first run there, by
hand on the server, brought the client from set 75 to V3's set 77 and a head (rows 142 to 144);
the timer's runs have kept it there since, from the relayer key
0xE94a…0257at its own nonces. The histories aredeployments/relay-bsc-testnet-0xe94a….jsonlanddeployments/automint-bsc-testnet-0x57cc….jsonl. - The way in, 125 seconds. Lock 8 was sent with
npm run v3-vault -- lock --dst-chain 97 --dst-minter 0xdc31a3BC1354545EeB985AaD7b2Aa662E9272348. Auto-mint waited for V3's finality and its 90-second grace, then sent the first mint by the head path, which bound the vault: the minter now takes the vault's trie id0xbe307c2d…, as on the other three chains. The mint's block came 125 seconds after the lock's by the block timestamps (V3 #275,563 at 09:25:42 UTC, BSC 140,165,449 at 09:27:46.55), against 139 and 133 seconds on the rollups (rows 117 to 120). It used 2,658,627 gas, 15.8% of the 16,777,216 cap. - The way back, 46 seconds, at finality. Burn 0 was sent with
npm run release -- burn --network bsc-testnet --amount 0.001 --v3-recipient 0xD9838A86cF83C28Ee486544060D1cA4Ca2971071. The watcher, which on this chain reads a burn only at its finalized block (no fast mode, and no--confirmationspair for it), found the burn final 13 seconds after its block, ran every check of the release tool, and sent both approvals: the release's block came 46 seconds after the burn's (BSC 140,165,763 at 09:30:07.85 UTC, V3 #275,615 at 09:30:54), with the burn final on its own chain ("by": "finality"indeployments/v3-testnet.json). Fast mode on Sepolia took 84 seconds and trusted 3 confirmations (rows 56 to 58); most of the 46 is V3's blocks, about 6 seconds each. - Cost, at 0.1 gwei. The deployer's 12 transactions took 14,211,226 gas, 0.00142 tBNB; the relayer's first run 5,389,332 gas, 0.00054 tBNB; the mint 0.000266 tBNB, paid by the auto-mint key; the burn 0.0000053 tBNB.
- After row 149 the vault's
outstanding, 1.411 tPOT, again equals the wtPOT supply on the four chains (1.411 wtPOT, all of it on Sepolia), with no burn waiting for a release: 2.603 tPOT released on V3 in all.
| Path | Contents |
|---|---|
contracts/ |
Foundry project: light client, proof verification, bridge contracts, and the proof kit (src/kit/) |
relayer/ |
TypeScript tools: fetch finality and storage proofs from Portaldot nodes, relay finality to the light clients on Sepolia, Base Sepolia, Arbitrum Sepolia and the BNB Smart Chain testnet, deploy and use the vault on V3, mint wtPOT, auto-mint each new lock, run the return path, and read proven V3 state with the proof kit (npm run kit) |
fixtures/ |
Captured chain data used by the tests, each file with its source |
site/ |
The status page and the bridge app (site/app/), in Vite, TypeScript and Preact: they read the chains, with the records as the fallback; deployed to GitHub Pages by .github/workflows/pages.yml |
deployments/ |
The tools' records, with addresses, hashes, blocks, gas and fees: each chain's deployments, the rollups' funding through their canonical bridges and the BNB Smart Chain testnet's tBNB, the mints and burns, the proof kit's deployment, and on the V3 testnet the vault's deployment, the locks its tool sent, the committee's funding and every release; and copies of the relayer's histories (to Oct 28, and the BNB Smart Chain testnet's first run) and of auto-mint's. The plain transfers that funded the auto-mint key and, on the rollups, the owner's MetaMask wallet are only in the README's rows (83, 100, 101, 115, 116 and 141); the wallet's funding on V3 and the two locks the app sent (rows 114, 117 and 119) are in v3-app-transfers.json, read back from V3's EVM RPC. Seven plain transfers are in neither: the deployer's 0.1 and 0.05 ETH to the two relayer keys on Sepolia on Oct 23 (its nonces 81 and 82); and on V3, from 5ERJ…gPJ7, 200 tPOT to the deployer's EVM account on Oct 18 (#92,643), the committee multisig's first 20 tPOT on Oct 19 (#101,134, in docs/V3.md) and 5 tPOT in three transfers to the owner's Portaldot wallet on Oct 27 (#221,828, #221,850 and #221,920) |
docs/ |
How to deploy it (DEPLOY.md), gas measurements, the bridge's design and runbook, V3 notes, decisions, the business model, the proof kit |
ops/server/ |
The systemd units, timers and scripts that run the relayers, the release watcher, auto-mint, the rollups' relay runs and a health check on a Linux server (its README) |
.github/workflows/ |
ci.yml, the build and the tests on every push and pull request, and pages.yml, which builds and deploys the pages |
Every third-party library is listed in NOTICE.md with its source,
pinned commit and licence, and so are the site's npm packages and its two fonts. Commits that add
third-party code start with vendor:.
Lines on Nov 1, counted with wc -l (blank lines and comments included):
| Code | Files | Lines |
|---|---|---|
The project's own: contracts/src (without trie/vendor/), contracts/test, contracts/script, relayer/src, site/src, site/build, site/test, ops |
233 | 104,308 |
Vendored, unmodified: the Solidity of the four libraries in contracts/lib/, at the commits NOTICE.md pins |
149 | 62,200 |
Vendored and modified: contracts/src/trie/vendor/PolkadotTrie.sol |
1 | 176 |
The contracts compile 41 of the vendored files: 19 of forge-std (22,754 lines), for the tests and
scripts only, and 22 of crypto-lib, solidity-merkle-trees and blake2b-solidity (3,117 lines), the
ed25519, trie and BLAKE2b code the light client and the proofs use. With the modified PolkadotTrie,
the Solidity under contracts/src compiles 3,293 vendored lines beside 2,965 of its own. The
libraries also carry JavaScript, tests and docs that nothing here builds. To count:
git submodule update --init
code='\.(sol|ts|tsx|mts|js|mjs|cjs|css|html|sh|service|timer|conf)$'
count() { grep -E "$code" | tr '\n' '\0' | xargs -0 cat | wc -l; }
git ls-files -- contracts/src ':!contracts/src/trie/vendor' contracts/test contracts/script \
relayer/src site/src site/build site/test ops | count
git ls-files --recurse-submodules -- contracts/lib | grep '\.sol$' | count
git ls-files -- contracts/src/trie/vendor | countBefore the build window, on Oct 10 and 11, read-only scripts checked that the design could work:
on the old mainnet's public node, a GRANDPA justification's signatures, a header hash, a storage
read proof and a set change, verified off-chain, then the justifications' timing and its MMR proofs,
which failed; on Sepolia and eight other EVM testnets, whether the BLAKE2F precompile answers, by
eth_call; and the V3 testnet's endpoints, which answered HTTP 502. They signed and sent nothing,
and none of them is in this repository, whose first commit is from Oct 14, 18:12 UTC. The capture
tool and the node probe that replaced them were written in the window (relayer/src/capture.ts on
Oct 14, relayer/src/probe.ts on Oct 15). Every fixture was captured from Oct 14, 18:40 UTC on,
and the deployer's first transaction is its first Sepolia deployment, on Oct 17 (block 11,934,321).
Bridge Zero is licensed under the Apache License 2.0: see LICENSE. It covers all of
the project's own code, including the Solidity sources whose SPDX-License-Identifier line still
reads UNLICENSED: those lines are left as the contracts were built and deployed, so that the
deployed bytecode and the verified sources still match this repository byte for byte.
Third-party code keeps its own licence, listed in NOTICE.md.