Bug bounties where the hunter never has to say who they are.
Rewards escrowed in NIGHT, reports sealed for the company, a private track record,
and a GenLayer jury for disputes. Built on Midnight.
| Start here | |
|---|---|
| Live app | https://exploit-ten.vercel.app |
| The bounty board | https://exploit-ten.vercel.app/#/board |
| Demo video | demo/proof-of-exploit-demo.mp4, about two and a half minutes |
| Public preprod run | docs/preprod-run.json |
| Live stats, read from the chain | https://exploit-ten.vercel.app/#/board/stats |
| Practice range | practice/: three deliberately broken contracts to try the board on |
| Wallets | 1AM (proves in the wallet) or Lace (with a local proof server), on preprod |
Security researchers find the bugs that would otherwise drain protocols. To be paid for one today, a researcher signs up to a platform with a name, an email and often an identity document, and every report they file is tied to that account and kept on someone else's server.
Many of the best hunters would rather not. Some live where a public security career is risky, some work for a company that would object, some simply do not want a trail. They go unpaid, or disclose for free. And the company has the opposite problem: anyone can claim a bug, and a stranger with no name has no reputation to judge them by.
- A company posts a bounty against any target and escrows the reward in NIGHT on the board. Anyone can see it is funded before spending a minute on it.
- A hunter reports without an account. The report is sealed in the hunter's browser so only the company, and the hunter, can open it. The hunter gets a fresh identity on every bounty and chooses where the reward is paid.
- Accepting is paying. The company accepts and the reward moves in the same transaction.
- A rejection can go to a jury, never back to the company. The money stays locked. The hunter sees the company's reason and can ask the GenLayer jury to review, once. The jury reads the brief, the company's reason and the hunter's summary, and the full report only if the hunter chooses to share it.
- Every ruling is checkable. A relay carries the jury's verdict to Midnight and must cite the GenLayer case it came from. The case is stored beside the ruling, and any visitor can press one button to check the ruling against GenLayer.
- Every paid finding grows a private track record. A hunter proves "at least N paid findings" in zero knowledge, without revealing which bounties, and companies can require it.
The board on Midnight preprod, read by a visitor with no wallet: two bounties paid, one refunded after notice.
A rejected report the GenLayer jury upheld. The ruling on Midnight cites its jury case, and any visitor can check it: matches the jury's verdict.
flowchart LR
subgraph Browser["Hunter's or company's browser"]
K[Recovery phrase<br/>one secret per wallet]
S[Seal report<br/>X25519 + AES-GCM]
P[Prove in zero knowledge<br/>via 1AM or Lace]
end
subgraph Midnight["Midnight: the board contract"]
E[Escrow<br/>NIGHT held per bounty]
R[Reports<br/>sealed, on chain]
T[Track record<br/>Merkle tree of paid findings]
U[Rulings<br/>verdict + GenLayer case]
end
subgraph GenLayer["GenLayer: the jury"]
J[Intelligent contract<br/>validators judge and must agree]
end
K --> S --> R
K --> P --> E
P --> T
R -. rejected .-> J
J -->|verdict, case ID| RL[Relay]
RL --> U
U -->|anyone checks| J
Take Midnight away and the product cannot exist: a hunter would have to be identifiable to be paid, and a reputation would have to be public to be trusted.
| What | Where it lives |
|---|---|
| The reward | Unshielded NIGHT held by the board, released only by acceptance or a jury ruling |
| The report | Sealed on chain; only the company's key and the hunter's key open it |
| The hunter | An identity derived from a secret on the hunter's device, different on every bounty |
| The track record | Leaves in a Merkle tree on chain; membership and count proved in zero knowledge |
| The master secret | Only on the device, backed up as a 24 word recovery phrase |
Compact features doing the work: witnesses for every secret, explicit disclose() for everything
that reaches the ledger, persistentHash commitments with domain separation, a
HistoricMerkleTree of records with path proofs, block time for deadlines, and
receiveUnshielded / sendUnshielded for real custody of the reward.
How the track record counts without revealing anything. A record is the hash of the hunter's master secret, a count, and a nonce derived from the bounty that earned it. Submitting with a record proves ownership of some record with count k and computes the next record, k plus 1, inside the proof; it enters the tree only when the finding is paid. Reusing an old record can only ever give a lower count, so no nullifier set is needed and no two of a hunter's records can be linked. The recovery phrase alone finds every record again.
Whether a vulnerability report is valid is a judgement, not a computation. A circuit cannot make
it, and a single human arbiter asks everyone to trust one person. GenLayer runs the judgement
across independent validators that must agree. The jury contract is
arbiter/contracts/arbiter.py, deployed on GenLayer studionet at
0x00B87410E062dDb09e2ae5229287AFB173bBc5b2. It accepts only a real boolean verdict.
The Midnight contract cannot read GenLayer, so a relay carries each verdict. Two things keep the relay honest: the contract makes it cite the GenLayer case, stored with the ruling for good, and every bounty page lets anyone compare the ruling with the case in one click. A relay that lies leaves permanent public proof of it.
| Attempt | Result | Test |
|---|---|---|
| Company reads a report, then refunds | Refused while a report is open | cannot close a bounty with a report under review |
| Company rejects to keep the money | The money stays locked; the hunter can go to the jury | turns a rejection into a dispute, not a refund |
| Company ignores a report | After the review window the hunter goes to the jury alone | lets the hunter escalate a silent vendor |
| Company races a refund against a report in flight | A close must be announced; reports count until it ends | refuses reports after the close time, so nobody can race it |
| Hunter rewrites the case until the jury agrees | The case is accepted once | is accepted once, so the hunter cannot shop for a verdict |
| Relay rules without a case, or reuses one | Refused | must exist before a ruling, cannot be cited for a second ruling |
| Anyone but the company, hunter or relay acts | Only the right key can | refuses acceptance by anyone but the vendor and others |
| Hunter overstates or borrows a record | The count and the master secret are bound into the record | refuses an overstated count, refuses someone else's record |
| A record proof is replayed | Bound to the verifier's challenge | attests a record to a verifier, once per challenge |
All in contract/src/test/board.test.ts, which also checks every payout's destination and amount.
Reviewed bounties are the board. Any target, judged by the company, with the jury behind it. They do not prove an exploit runs: the company reads the report, and the jury weighs what it is shown.
Verified bounties are for rules small enough to write as a circuit. contract/src/exploit.compact pays automatically when a hunter proves in zero knowledge that they hold an input breaking a stated rule, and reveals it in the transaction that pays them. It ships with a reference vault and a walkthrough that needs no wallet. Proving arbitrary deployed code exploitable needs a zkVM, which Midnight does not have today; this tier is where the board goes as that changes.
| Contract tests | 49, in the simulator: every path, every refusal, every payout |
| From source | CI installs the Compact toolchain, compiles both contracts, and tests the fresh build |
| On a Midnight node | Every board path with real proofs, including the record proof and a live GenLayer verdict: deploy/runs/board-local.json |
| In a real browser | The built app driven through every screen against the node, as company, hunter, relay and visitor: cd deploy && npm run e2e |
| On public preprod | Two runs with the 1AM wallet. The current board: bounties paid to two different fresh addresses, a rejected report sent to the jury with the full report shared, upheld, carried to Midnight citing its case, and checked by a visitor: matches. The first run proved the record gated entry and a jury that turned down a vague case. docs/preprod-run.json |
| The jury | A strong in scope case upheld, an out of scope one not upheld: cd arbiter && node deploy.mjs |
Requires Linux or WSL2 for the Compact toolchain, and Node.js 22.
npm run verify # pinned install, every test, type check, production buildcd contract && npm install
npm run compact # compiles exploit.compact and board.compact
npm test # 49 testsdocker compose up -d # node, indexer, proof server; see docs/LOCAL_STACK.md
cd deploy && npm install
npm run build:contract
npm run board # every board path on the node, with a live GenLayer verdict
npm --prefix ../app run build && npm run e2e # the app in a real browsercd arbiter && npm install && node deploy.mjs # deploy the jury and judge two cases| Path | What |
|---|---|
| contract/ | board.compact (the product), exploit.compact (the verified tier), simulator tests |
| app/ | The web app: board, identity and records, relay desk, verified vault walkthrough |
| arbiter/ | The GenLayer jury contract and its deploy and relay scripts |
| deploy/ | Node runs with real proofs, and the browser test |
| docs/ | Preprod evidence, the local Midnight stack, media |
| practice/ | Deliberately vulnerable demo contracts for the practice bounties |
| cli/, ui/ | Terminal and one page demos of the verified tier |
Stated plainly, because a privacy tool that overstates itself is worse than none.
- Identities are private; payouts are visible. NIGHT is an unshielded token by design, so a payout's address and amount are public. That is why the hunter chooses the payout address on every report: a fresh address per bounty keeps bounties unlinkable, and the preprod run paid two bounties to two different addresses. Paying in a shielded token would hide the payout too, and is on the roadmap.
- Verdicts are carried, and checkable. A Midnight contract cannot read another chain, so a relay named by the company carries each GenLayer verdict. The contract makes every ruling cite its jury case and refuses to reuse one, and every bounty page lets anyone compare the ruling with GenLayer. A relay that misreports leaves permanent public proof of it.
- The first valid report holds the bounty. A bounty reviews one report at a time, like every bounty programme, so two hunters are never paid for the same bug. If the report is withdrawn or not upheld, the bounty reopens for the next hunter.
- Your identity is a recovery phrase. Like a wallet, it lives on your device and nowhere else, and the 24 words restore it, reports and track record included, in any browser. Wallet signatures are randomized, so the identity cannot be derived from the wallet itself.
- A track record costs real bounties. Records enter the tree only when a bounty is paid, so every record means a real escrow and payout. Weighting records by the reward behind them is next, so a requirement can also mean "paid findings worth at least this much".
- Preprod, not mainnet. Tested and running on Midnight preprod and GenLayer studionet. Not audited.
