A custodial wrapped token fails silently: reserves leave, the token keeps moving, and holders learn last.
Ballast is a safety layer for the first dollar on Portaldot. It is built around a clearly labelled test asset, pUSDC-test, whose roles are enforced by the Portaldot V3 runtime itself (the native Assets pallet plus Multisig), with the reserve held in a ReserveVault contract on Ethereum Sepolia.
One flow, four guarantees
- Mint needs a 2-of-2 multisig whose mandatory co-signer, the reserve bot, signs only if finalized reserves cover supply plus the amount.
-
Reserves are attested on chain:
BALLAST/1remarks, mirrored into aReserveOraclecontract on Revive.ballast verifyre-checks both chains and rejects a forged remark. - A watcher set (a 1-of-2 multisig) can only freeze.
- Only the 2-of-3 admin multisig can thaw.
Because these are runtime roles, not app logic, a watcher that tries to thaw is refused by the chain itself.
What happened on the V3 testnet
Role drill, 2026-10-24. The custodian drained 31.20 of 104.00 from the vault (local anvil reserve). Watcher W2 froze asset 4401 in extrinsic 170112-1, 12.22 s after the drain. The next transfer failed Assets.AssetNotLive (170114-1), a watcher's attempt to thaw failed Assets.NoPermission (170115-1), and the bot co-signed the thaw only after finality passed the drain (170133-1).
Real Sepolia drill, 2026-10-30. ReserveVault deployed on Ethereum Sepolia (0x7ceB7c3EBDBCC75e585599e5dd944286F009357b, 100.00 Circle test USDC). The drill passed 8/8 steps and 18/18 checks: Sepolia release → freeze 258434-1 on V3 10.26 s later → transfer refused → refill → the bot refused to thaw until both providers' finalized head passed the drain (1,026 s of real waiting) → co-signed thaw 258627-1.
Benchmarks, 35 freeze drills (docs/BENCHMARKS.md): drain → freeze included at p50 / p95 = 12.31 / 16.71 s with the local anvil reserve (n = 26) and 10.61 / 11.25 s on real Sepolia (n = 6). The fee is about 0.03 tPOT per freeze.
All 261/261 V3 extrinsics from 23 runs were re-queried and final at head #261558; failed and errored runs stay listed in docs/PROOF.md.
Things I learned
- Wait for finality on both chains. The thaw path deliberately waits until both providers' finalized head is past the drain; on Sepolia that was 1,026 s of real waiting.
-
A watcher on GitHub Actions is not real time. Run
41150496088froze the asset with the primary watcher off, but the*/5cron is best-effort, so it is a backup, not the main line.
Honest limits
pUSDC-test is a labelled test asset, not a stablecoin. The reserve gates are bot code; the runtime enforces the roles and multisig thresholds, but this is not a trustless bridge. One person holds every key in this build.
Links
- Code: https://github.com/edycutjong/portaldot-hackathon-2026-ballast-edycu
- Reserves page: https://ballast.edycu.dev/reserves/
- Demo video: https://youtu.be/jV5dCy1o3O8 (recorded 2026-10-24: it shows the V3 role drill, not the later Sepolia drill)
- Proof:
docs/PROOF.md· Benchmarks:docs/BENCHMARKS.md
342 tests (301 pytest + 20 Foundry + 21 Hardhat). Built for the Portaldot Hacker House 2026.
Top comments (0)