How QSAFU keeps it SAFU
The short version: your fees go to a vault with no private key, and only a one-time hash signature can move them. The long version is below, with every parameter. It's the same primitive behind XMSS and LMS (NIST SP 800-208).
Summary
A QSAFU vault is a program-derived account on Solana. It has no private key, so nothing signed with Ed25519 can move its funds. The vault program releases SOL or tokens only for a valid Winternitz one-time signature (WOTS) over the exact withdrawal, then rotates the vault to the next key in the same instruction. The address never changes; each key signs once.
Coins launched here name the launcher's vault as the pump.fun creator. pump.fun pays creator fees to that address, collection needs no signature, and from then on only the vault's hash-based key can withdraw.
Threat model
A large quantum computer running Shor's algorithm derives an Ed25519 private key from its public key. On Solana every address is a public key, so every wallet is exposed the day such a machine exists. Hash functions lose much less: Grover's algorithm gives at most a square-root speed-up on preimage search and parallelises poorly.
QSAFU protects funds at rest in vaults. It does not change how Solana signs transactions: the wallet that pays network fees is still Ed25519. That wallet never holds vault funds, and a forged fee-payer signature cannot produce a valid Winternitz signature.
Signature scheme
| Parameter | Value |
|---|---|
| n | 24 bytes per chain element (192-bit), the length SLH-DSA (FIPS 205) uses at security category 3 |
| w | 256: each chain has 255 steps, one per byte value |
| chains | 34: 32 message-digest bytes plus a 2-byte checksum |
| hash | keccak-256 (Solana syscall), truncated to n where noted |
| signature | 816 bytes (34 × 24) |
F(ps, i, j, x) = keccak256(ps ‖ 0x00 ‖ i ‖ j ‖ x)[0..24] chain step j → j+1 of chain i pk_i = F applied 255 times to sk_i root = keccak256(ps ‖ 0x01 ‖ pk_0 ‖ … ‖ pk_33) d = keccak256(message) 32 digest chunks d_0..d_31 C = Σ (255 − d_k), encoded as 2 big-endian bytes chunks 32, 33 sig_i = F^(c_i)(sk_i) verify : walk each sig_i from c_i to 255, rebuild root, compare with the vault
The checksum is what makes a revealed signature useless to an attacker: moving any digest chunk forward requires moving a checksum chunk backward, which means inverting the hash. The public seed ps and the chain and position tweaks (i, j) make every hash call in every vault distinct, which blocks multi-target attacks across vaults.
Key derivation
One 32-byte seed, shown to the user as 24 BIP39 words, generates every key of every vault. Derivation is local to the browser.
ps(v) = keccak256(TAG_PS ‖ seed ‖ u32le(v)) sk_i(v, k) = keccak256(TAG_SK ‖ seed ‖ u32le(v) ‖ u32le(k) ‖ u8(i))[0..24] vault v : key index k = the vault's on-chain nonce address = PDA(["vault", ps(v), root(v, 0)], program)
On the device the seed is encrypted with AES-256-GCM under a PBKDF2-SHA256 key (600,000 iterations) from the user's password. Symmetric 256-bit encryption keeps a comfortable margin against Grover.
Vault account
| Offset | Field | Notes |
|---|---|---|
| 0 | tag | 0x5a |
| 1 | bump | PDA bump |
| 2 | version | 1 |
| 4..12 | nonce | u64, index of the current key; +1 per spend |
| 12..44 | ps | public seed |
| 44..76 | id | root of key 0 (fixed, part of the address) |
| 76..108 | root | root of the current key |
The address commits to both ps and id, so nobody can open a look-alike vault at someone else's address. SOL is the account's lamports above rent. Tokens sit in associated token accounts owned by the vault PDA.
Instructions
| # | Name | Accounts | Data |
|---|---|---|---|
| 0 | Open | payer (signer), vault, system | ps, id, bump |
| 1 | WithdrawSol | vault, destination | sig, amount, next_root |
| 2 | WithdrawToken | vault, source, destination, token program | sig, amount, next_root |
| 3 | Unwrap | vault, vault WSOL account, token program | none (permissionless) |
Open is idempotent and keeps any lamports already sent to the address. Withdrawals reject a zero amount, a repeated or zero next root, and anything that would leave the vault below rent. Token withdrawals support SPL Token and Token-2022 (mints without transfer fees or hooks).
Signed messages
SOL TAG_SOL ‖ vault ‖ u64le(nonce) ‖ u64le(amount) ‖ destination ‖ next_root TOKEN TAG_TOK ‖ vault ‖ u64le(nonce) ‖ mint ‖ u64le(amount) ‖ destination ‖ next_root
The signature binds the vault, the key index, the amount, the destination and the next key. It can't be replayed (the nonce moves), redirected (the destination is signed) or used to install an attacker's key (the next root is signed). The mint is read from the source token account on chain.
Fee flow
At launch, pump.fun's create_v2 receives the vault address as creator. Bonding-curve fees accumulate in pump.fun's creator-vault PDA; collect_creator_fee pays them to the creator and does not require the creator to sign. After graduation, PumpSwap pays creator fees in WSOL to the vault's WSOL account; the vault program's Unwrap turns them into SOL in place. A keeper calls both on a schedule. Anyone else can too, and the funds can only land in the vault.
Costs
| Operation | Compute units | Transaction size |
|---|---|---|
| Open | ≈3,300 | 356 bytes |
| WithdrawSol | ≈550k-700k (≈144 CU per hash) | 1,147 bytes |
| WithdrawToken | ≈600k-750k | 1,213 bytes |
| Unwrap | ≈1,800 | 323 bytes |
Verification hashes between roughly 3,800 and 5,000 times for a typical message, inside the 1.4M CU limit even in the worst case. Withdrawals fit Solana's 1,232-byte packet without lookup tables.
Limitations
- Each key must sign only one message. The program enforces rotation on chain; the app stores the pending message so a failed send can only be retried, never re-signed differently.
- Fees waiting in pump.fun's creator vault are held by pump.fun's program until collected. They can only be paid to your vault.
- Wallet balances outside the vault remain Ed25519 until Solana adopts post-quantum accounts. SIMD-0416 (a Falcon verification syscall) is the first step.
- The program has not had an external audit. Treat large balances accordingly.
Deployment
Read live from Solana. While the upgrade authority is an Ed25519 key, whoever holds it could change the program, so it will be removed once the program has run in production.