Skip to content

About

A Portaldot light client on Ethereum and EVM chains: tPOT locked on the Portaldot V3 testnet becomes wtPOT on Sepolia, Base Sepolia, Arbitrum Sepolia or the BNB Smart Chain testnet, minted from proof; the way back is trusted, a 2-of-3 committee. Live app, status page and Verify.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Bridge Zero

CI Pages

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.

The plan on one page

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).

What it does

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.

Live on testnets

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.

How it works

Components

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
Loading

From a lock to a mint

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
Loading

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.

Verify it yourself

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 0

npm 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.

For builders

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.sol and the IFinalizedStateRoots.sol it imports (both Solidity 0.8.27, exact), and calls PortaldotStateReader on Sepolia, 0x2ea6…1fA9 (row 127): a stateless contract with no owner, bound to GrandpaLightClient #2 and linked to the deployed StorageProofs, source-verified.
  • With the library: PortaldotState has the same reads as internal functions, the key builders and the decoders.
  • The example: ProvenAssetGate records 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/ (after npm ci), npm run kit -- read balance --account 0xD9838A86cF83C28Ee486544060D1cA4Ca2971071 fetches 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 by eth_call (read asset, read nft, read slot and read lock likewise); npm run kit -- check checks 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.

Status

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-every blocks 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 in deployments/relay-sepolia-0xe94a….jsonl and deployments/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-sepolia or arbitrum-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 importAndMint that verified V3's finality, bound the vault, proved commitments[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 with NonceAlreadyUsed(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, with bsc-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 in System.Events and 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 release checks 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-sepolia and each chain's confirmations, Sepolia's still 3, and since Nov 1 with bsc-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, mint and status) 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 release and its watcher take --network, each rollup with its own records, its own finality (its finalized block 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-sepolia since 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, BridgeMinter and 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.yml builds and deploys both pages on every change to the records.
  • The proof kit, since Oct 31: PortaldotState, PortaldotStateReader and the example ProvenAssetGate read 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, and npm run kit fetches, walks and checks the proofs (For builders). The Reader and the gate are on Sepolia, source-verified (rows 127 and 128): the Reader, bound to GrandpaLightClient #2 and linked to the deployed StorageProofs, 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.yml builds 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 with npm 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 (in deployments/ 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 with npm run relay -- --network base-sepolia --chain v3 --send --account <keystore> --password-file <path> (or arbitrum-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_getLogs over 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.

Roadmap

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 release pays 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 and ProvenBalance #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), and BridgeMinter and 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 ecrecover precompile 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: PortaldotStateReader and the example ProvenAssetGate, deployed on Sepolia on Oct 31 and source-verified (rows 127 and 128), the PortaldotState library, and npm 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/).

Development log

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 app branch: 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 and ProvenBalance #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 in System.Events, and lock 3 minted that way (rows 86 and 87); BridgeMinter and wtPOT on both rollups, and both rollup clients at V3's set 62 (rows 88 to 97; 288 relayer tests). On the app branch, rebased on main: Polkadot wallets (the Portaldot Wallet, polkadot{.js}, SubWallet, Talisman), whose lock is a revive.call signed 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: release and 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 the app branch: 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 app branch 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, PortaldotStateReader and ProvenAssetGate with 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, BridgeMinter and 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.

Evidence

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.

  • initialize is 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).

Following the live chains

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.

The vault on the Portaldot V3 testnet

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].

The bridge: a lock on V3 becomes wtPOT on Sepolia

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.

The relayer's later runs

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

The way back: a burn on Sepolia, a release on V3 (trusted)

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 -- status checks 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 --send with 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).

Lock to mint in 48 seconds

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).

More chains: the V3 light client on Base Sepolia and Arbitrum Sepolia

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…0ab6 is the V3 testnet's client and 0x40f4…86f0 its ProvenBalance, 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.

Auto-mint: lock 2, minted for its recipient

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).

Auto-mint: lock 3, sent from a Substrate account

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's importAndMint.
  • After it, the vault's outstanding (1.411 tPOT) equals the wtPOT supply (1.411 wtPOT).

More chains: BridgeMinter and wtPOT on Base Sepolia and Arbitrum Sepolia

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.chainid and 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…2348 and 0xE113…A95e hold 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 (genesis 0xd4f9acbc…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 reverts VaultNotBound().
  • 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.

More chains: the way in to Base Sepolia and Arbitrum Sepolia

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: importAndMint at 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_estimateGas includes them, and both tools ask NodeInterface's gasEstimateL1Component for 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's getL1FeeUpperBound counts in the balance a send needs and in auto-mint's --max-spend, and each mined transaction's l1Fee in 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-spend reached, 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: importAndMint at V3's finalized head, signed by set 62, with the vault's account proof (9 items). eth_call and eth_estimateGas passed: 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 4 and --network arbitrum-sepolia --nonce 5 ran the same mints with eth_simulateV1: HeaderImported, VaultBound, Minted and Transfer, 2,639,946 and 2,612,827 gas, and the same call again reverting NonceAlreadyUsed.
  • 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…0257 at 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 outstanding is 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).

More chains: the way back from Base Sepolia and Arbitrum Sepolia

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…2348 and count their burns from 0; burnIdOf(n) puts block.chainid in the id, so burn 0 on Base Sepolia is 0x6ae40d53…bedb6d8d and burn 0 on Arbitrum Sepolia 0x9890de41…cfe80522. The vault's released[burnId] pays each once; the watcher keeps each chain's releases apart in deployments/v3-testnet.json by the burn's chain id and minter, and its burn cache per network.
  • Final, on a rollup, is its finalized block 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 --confirmations of that chain's blocks below its latest, up to the fast cap; with --send at 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. --confirmations takes 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 --send serves every chain, and --rounds n runs 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's l1Fee or gasUsedForL1. 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 in deployments/<network>-bridge.json before 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's outstanding, 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 180 and 144) 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.call and its trace: the Released log and the payout of 0.001 tPOT to 0xD983…1071, storage deposit 3.8415 tPOT; call hashes 0xe2ce3541…fe0f25f1 and 0xf3c92150…77a13855), then member 1's opening asMulti. 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) as release --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's outstanding (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.

The app: the first transfers from the owner's own wallet

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, importAndMint at 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 commitments entry 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 for released[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's outstanding (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.

The proof kit on Sepolia

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_call at Sepolia block 12,034,965, each returning the fixture's value, with eth_estimateGas's gas for the whole transaction: the deployer's System.Account (nonce 6, 159.47352201127852 tPOT free), 452,621; 0x3e79…dcdf's Assets.Account of pUSD (60.5, Liquid), 480,180; the owner of item 3,163,806,249 in NFT collection 10, 363,589; the vault's Revive.AccountInfoOf entry (its trie id and code hash), 404,545; and the vault's commitments[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.

More chains: the BNB Smart Chain testnet

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…0257 at its own nonces. The histories are deployments/relay-bsc-testnet-0xe94a….jsonl and deployments/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 id 0xbe307c2d…, 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 --confirmations pair 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" in deployments/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.

Layout

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

Third-party code

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 | count

Before 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).

Licence

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.

About

A Portaldot light client on Ethereum and EVM chains: tPOT locked on the Portaldot V3 testnet becomes wtPOT on Sepolia, Base Sepolia, Arbitrum Sepolia or the BNB Smart Chain testnet, minted from proof; the way back is trusted, a 2-of-3 committee. Live app, status page and Verify.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages