BlackSwan Relay
Recapitalize without the signal.
Created on 20th August 2026
•
BlackSwan Relay
Recapitalize without the signal.
The problem BlackSwan Relay solves
What it is
A lending vault with health 0.92 holds 92¢ of collateral for every $1 of debt. It needs 600 mUSDC to stay solvent. The natural way to raise that amount in public - each rescuer posting how much they will put in - reveals the exact discount each rescuer wants. Bots copy the signal, the discount disappears, and the rescue fails.
Insurance does not help here. It pays claims after failure. Proof-of-reserves shows a vault is solvent but does not raise new capital. Confidential lending creates new loans. Treasury tools move money that already exists. None let a group privately gather new capital for a specific vault without showing the split.
BlackSwan Relay is a private rescue market for one vault, one round, and a small set of rescuers.
How it works:
- A keeper opens a round with a fixed target, for example 600 mUSDC.
- Each rescuer commits locally as
hash(amount, nullifier, secret, round)using pedersen_hash. Only the 32-byte hash is sent on-chain. Calldata looks like0xe9ceb85f <hash> <nullifier>with no visible300. - A Noir circuit proves the hidden amounts add up to at least the target and that every hash is correctly bound. Fourteen public inputs are used - six commitments, six nullifier hashes, the target, and the round - so commitments and nullifiers cannot be shuffled between rescuers.
- A Barretenberg UltraHonk verifier checks the 8384-byte ZK proof on Sepolia. If it passes and no nullifier is reused, the vault is recapitalized and discounted RescueShares are minted proportional to each escrow in one atomic transaction. If the sum is too low or a nullifier is reused, the transaction reverts.
What stays private and what is public:
- Private: individual amounts and the exact strategy size before settlement. The explorer shows only hashes until a proof is presented.
- Public: the round, the target, the hashes, the fact that the target was met, the total moved, and the shares minted. Participant addresses are visible; hiding who participated would need a larger anonymity set.
Why it stands out:
- Aggregate-capital proof, not a balance check. The circuit proves
sum ≥ Tover six slots withu64range checks, not just solvency. It covers underfunded padding and prevents overflow. - Commitment and nullifier binding. Both hashes are public inputs and recomputed inside the circuit. The contract checks binding before the reuse guard, which closes the shuffle attack where an attacker proves with one set of nullifiers and submits another.
- Dual escrow. A hash-only path shows the privacy property with a single aggregated transfer, and a real-escrow path does
transferFromper rescuer, storesescrowanddepositor, and mints shares pro-rata. With a standard ERC-20 the real path necessarily reveals per-rescuer transfers after inclusion; the system documents this and notes that a confidential token would be needed for full post-settlement privacy. - End-to-end verifiability. One proof, one vault, one rescue, one verifier, one pool on Sepolia. Every deposit, opening, settlement, and both revert paths are live transactions with direct Etherscan links. No amount is simulated in calldata.
Verifiable on Sepolia (test tokens, no real money): MockERC20 0x11f3..., Vault 0x1eE4..., Rescue 0x3742..., Pool 0xba04..., Verifier 0x4207... (47kB, 8384B ZK). Honest round 300+200+100=600 settles in 0xfce7... block 11548213 gas 4734234 with RescueTargetMet(1,600), VaultRecapped, and RescueShareMinted events. A Next.js UI walks through the same six steps without a terminal, and every claim links to Etherscan.
Challenges I ran into
1. Moving real capital without revealing the split on a standard token
A rescue needs two things that pull in opposite directions: individual amounts must stay hidden before settlement, but escrowing real ERC-20 value necessarily emits Transfer(from, to, amount) after the block is published. Even with a private mempool, the breakdown becomes visible in logs once the transaction is included. No standard token can hide the amount after settlement.
The system separates the commitment layer from the settlement layer. ShieldedPool exposes a hash-only deposit that carries no value and a real-escrow deposit that does transferFrom, records escrow per nullifier and depositor per nullifier, and is documented as necessarily revealing per-rescuer transfers. Settlement reads escrow, mints RescueShares proportionally, and releases the total from escrow. The limitation is stated directly: hash privacy holds in calldata and the mempool, full amount privacy after settlement would require a confidential token. The test DepositRealLeaksTransferButCommitmentRemainsHashOnly asserts the trade-off.
2. Binding commitments to nullifiers to prevent shuffling
Proving sum ≥ T is not enough. Each public commitment must be tied to the same private nullifier that the contract tracks for reuse. If nullifiers are not part of the public inputs, a prover could mix nullifiers from one commitment with amounts from another and still pass verification, bypassing single-use guarantees.
The circuit was built with fourteen public inputs - commitments[6], nullifier_hashes[6], T, and roundId - and asserts pedersen_hash(amount, nullifier, secret, round) == commitment and nullifier == nullifier_hash in the same loop. The contract checks publicInputs[6+i] == nullifier[i] before calling the verifier, so a shuffled proof fails with InvalidPublicInputs before the reuse guard. A dedicated test proves shuffling reverts.
3. Choosing soundness over gas for the proof
A smaller, non-ZK proof saves about 600k gas and 960 bytes. It still hides witnesses from public observers, but it is not formally zero-knowledge. Presenting a witness-hiding build as a ZK proof would not survive cryptographic review.
The system ships a real ZK verifier with keccak transcript (8384 bytes) as the primary path. It is verified both off-chain with bb verify and on-chain on Sepolia. The cost is higher, but the soundness property is preserved and the verification is directly checkable on Etherscan.
4. Getting privacy without relying on a private mempool
A common assumption is that a private RPC is what hides amounts. In practice the deployment often falls back to public broadcast because the Sepolia private endpoint is not reliably available, and in that case mempool privacy alone would provide no guarantee.
The design makes privacy mempool-agnostic. Amounts never appear in calldata - only 0xe9ceb85f <hash> <nullifier> is sent - so even a public transaction shows no 012c for 300. The private RPC path is kept as defense-in-depth and logs its fallback, but the verifier, the explorer view, and the demo all hold without it. Reviewers can confirm this by inspecting any deposit calldata and its logs on Etherscan.
Tracks Applied (2)
