How the vault, the bait, and the canary fit together
This page is the reference version of the pitch on the landing page. Full source, the Foundry test suite, and the deploy scripts are in the repo.
The mechanism
A factory transaction deploys two contracts and wires them together: a vault that holds the real reserve, and a bait that holds a small instant reward. A separate canary key — generated in your browser, never stored anywhere — is the only key that can claim the bait.
Owner key (hot, in .env) Canary key (bait, same .env)
│ │
│ requestWithdrawal() │ claim() — pays instantly
▼ │
NchedoVault ◄──────── lock() ────── NchedoBait
pays after a public delay, the lock fires inside the
always, for everyone thief's own transactionNchedoBait.claim() calls NchedoVault.lock() before it pays out. If either half fails, the whole transaction reverts — a successful claim cannot pay without also locking. Any withdrawal in flight is deleted in the same transaction.
Three keys, one allowed to be careless
Hot and scriptable. Requests withdrawals, which mature after the delay you pick. It cannot skip the delay, and it cannot act at all once the vault is locked.
.envSpends exactly once, ever. Whoever signs with it gets the bait instantly and locks the vault in the same transaction — the entire detector is that one signature.
.env · same file, on purposeCold, kept off-repo. The only key that can sweep a locked vault (recoverTo), freeze one pre-emptively (lockByRecovery), or reverse a false alarm (unlock).
Threat model
| Scenario | Result |
|---|---|
| Owner key requests an immediate withdrawal | Delayed; announced on-chain, matures after the delay |
| Canary key claims the bait | Vault locks automatically, in the same transaction |
| A withdrawal is pending when the bait is claimed | Deleted in that same transaction |
| Owner key acts on a locked vault | Reverts — no request, no execute, no unlock |
| Attacker reads the code, ignores the bait, waits out the delay | Not automatically stopped — a public window to respond |
| Sweeper takes the canary's gas dust and never claims | Visible as a balance alert; not containment |
| Recovery on a locked (or live) vault | recoverTo sweeps everything to a safe address |
| Recovery key compromised | Outside the protection boundary |
| Funds in an EOA, not the vault | Not protected |
Every row is a passing test in contracts/test/Nchedo.t.sol.
Try it yourself
You do not need your own deployment, and you do not need to touch a local .env to test the app — the canary key is generated in your browser at /create and shown once on screen. Paste it straight into the attacker console on the panel to spring your own trap in the same session.
- Connect a wallet on Monad Testnet and get testnet MON from the faucet.
- Build a vault — pick a recovery address you also control, then send the one-shot transaction.
- Copy the canary key it hands you and paste it into “Take it” on the panel.
- Open the recovery console with the recovery wallet and sweep the reserve.
To run the contracts and tests locally instead, see README.md in the repo — forge test -vv covers both the claim path and the attacks Nchedo does not stop.
Deployed
Full write-up, brand notes, and the video script are in the repo — github.com/TheWeirdDee/nchedo ↗