DEV Community

Cover image for Ballast: freezing a test dollar on Portaldot when its reserve runs short
Edy Cu
Edy Cu

Posted on

Ballast: freezing a test dollar on Portaldot when its reserve runs short

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

  1. Mint needs a 2-of-2 multisig whose mandatory co-signer, the reserve bot, signs only if finalized reserves cover supply plus the amount.
  2. Reserves are attested on chain: BALLAST/1 remarks, mirrored into a ReserveOracle contract on Revive. ballast verify re-checks both chains and rejects a forged remark.
  3. A watcher set (a 1-of-2 multisig) can only freeze.
  4. 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 41150496088 froze the asset with the primary watcher off, but the */5 cron 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

342 tests (301 pytest + 20 Foundry + 21 Hardhat). Built for the Portaldot Hacker House 2026.

Top comments (0)