A leaked hot key should not empty a wallet in one block. A multisig fixes that, but it needs a second signer for every payment, which is heavy for one person.
The Portaldot runtime already has a better primitive. The Proxy pallet can make a delegate announce a call, wait a set number of blocks, and only then execute it. Another delegate can reject the announcement in between. In my search (not a market survey) I did not find a product built around it, so I built one for the Portaldot Hacker House 2026.
How it works
airlock create makes a keyless vault (proxy.create_pure) controlled by two keys:
-
Hot key, your daily key, registered with a delay (10 blocks = 60 s in the demo). It cannot move funds directly: the chain answers
Proxy.Unannounced. It can only announce a call hash and wait. -
Cold key, instant. It can veto an announcement (
reject_announcement, dispatched as the vault) or retire the hot key (airlock panic).
A watcher reads every Proxy.Announced event for the vault and matches the hash against the owner's intent registry. A spend you registered is YOURS. Anything else is NOT YOURS, printed with the amount at risk, the veto deadline and a paste-ready airlock veto … line.
Six Proxy-pallet calls are the whole product: create_pure, add_proxy, announce, proxy_announced, reject_announcement and remove_proxy. There is no contract and no runtime change; the security is Portaldot's consensus, not my server.
What happened on the V3 testnet
On 2026-10-24, the run in the demo video (evidence/final-video-run-v3-2026-10-24.md):
- A thief holding the hot key announced a sweep of the whole vault (
166811-1). - The watcher printed NOT YOURS: 11.4200 tPOT at risk, "Veto must land by #166820".
- The owner pasted the veto line. It landed in block #166816, four blocks early (
166816-1). - After the delay the thief executed (
166825-1) and then called the proxy directly (166831-1). Both failedProxy.Unannounced. -
airlock statusbefore and after: the same 11.4200 tPOT.
Earlier, on 2026-10-19, a 12.92 tPOT sweep was vetoed the same way, and scripts/verify_onchain.py re-fetched all 44 V3 proof rows by id: 44/44 matched.
On two local interim chains (not V3), a benchmark ran 20 thief drains per chain: 20/20 alerted and vetoed, 0 planck moved. A control vault with no delay lost 100 UNIT in one extrinsic.
Things I learned
- Inclusion is not finality. V3 has two validators and can fork, so off the interim chains every send waits for finality before Airlock reports it.
- Pin the chain. Chain profiles are pinned by genesis and runtime code hash; an unknown chain fails loudly instead of signing.
- An announcement exposes only a hash. That is why the intent registry exists: anything not registered is treated as NOT YOURS.
Honest limits
If the owner ignores the alert for the whole delay, the spend executes: the delay must be longer than your response time. The benchmark has not run on V3. There is no hosted UI; the CLI and watcher are the owner interface. Testnet only.
Links
- Code (MIT): https://github.com/edycutjong/portaldot-hackathon-2026-airlock-edycu
- Demo video (3 min): https://youtu.be/Es1387HDSTw
- Site: https://airlock.edycu.dev
- Proof:
docs/PROOF.mdin the repo
350 offline tests, plus 4 live-chain integration tests. Built for the Portaldot Hacker House 2026 (Ecosystem DApps). Feedback welcome, especially from validators and anyone holding POT on a hot key.
