Reference
On-chain program
The vault as a Solana program: accounts, instructions, what it checks and what it costs.
Status
Written in Rust with Pinocchio (no_std, no dependencies beyond the Solana SDK types), 56128 bytes compiled. Every instruction is tested on a local Solana runtime, and a differential test runs the same random histories through the program and through the JS engine and requires identical results to the lamport. It is not audited and not deployed to a cluster. The program id used in tests, 4r4QmNE6LDar2T3ZEepLw3r12FXpi29FwKCfM72MXZXB, comes from a local keypair and will change on deployment.
The vault account
One account per (coin, launcher), at the address derived from the seeds "vault", the mint and the launcher's address. The launcher must sign initialize, so nobody can occupy another launcher's address. If someone sends lamports to that address first, initialize tops it up and takes it over (allocate and assign) instead of failing. The data is a fixed 512 bytes, little-endian:
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | version (1) |
| 1 | 1 | bump |
| 2 | 1 | platform (0 github, 1 youtube, 2 twitch, 3 kick) |
| 3 | 1 | on expiry (0 launcher, 1 burn) |
| 4, 5 | 1, 1 | threshold, number of attesters |
| 6, 8, 10 | 2 each | streamer, launcher, burn shares in basis points |
| 12, 13 | 1, 1 | length of the account id, length of the mint in base58 |
| 16 | 4 | epoch |
| 24, 32 | 8 each | expiry and rotation delay, seconds |
| 40, 48, 56, 64 | 8 each | created at, window start, bound at, rotation effective at (0 = none) |
| 72 to 104 | 8 each | accrued, pending, paid to streamer, paid to launcher, burned |
| 112, 144, 176, 208 | 32 each | mint, launcher, bound wallet, pending rotation wallet (all zero = none) |
| 240 | 64 | account id, ASCII |
| 304 | 44 | mint in base58, ASCII |
| 352 | 160 | up to five attester public keys |
decodeVault in src/chain/client.mjs reads exactly this layout.
Instructions
| Tag | Name | Accounts | What it does |
|---|---|---|---|
| 0 | initialize | payer (signer), vault, launcher (signer), system program | Validates the configuration (the same rules as new Vault() in JS) and creates the vault. |
| 1 | crank | vault, launcher, incinerator, bound wallet | Applies due rotations and expiries, then measures the lamports that arrived since the last call and splits them. Anyone can call it. |
| 2 | claim | vault, launcher, incinerator, wallet to bind, instructions sysvar | Needs Ed25519 instructions just before it. Pays everything pending to the wallet and binds it. |
| 3 | rotate | vault, launcher, incinerator, new wallet, instructions sysvar | Same proof, for the new wallet; schedules the change after the delay. |
| 4 | cancel_rotation | vault, launcher, incinerator, current wallet (signer) | The current wallet signs the transaction to cancel. The epoch goes up. |
The launcher and incinerator accounts are always checked against the vault's own fields, and the bound wallet's account against the stored wallet, so passing other accounts cannot redirect a payout.
How attestations are checked
Each attester signs a text message (see Attesters). The signatures are verified by Solana's native Ed25519 program, a precompile, which runs before any program and fails the whole transaction on a bad signature. The vault program then reads the precompile instructions back out of the transaction (the instructions sysvar) and, for each one before claim or rotate:
- requires exactly one signature, with the signature, key and message all inside that same instruction (index
0xFFFF), so the precompile verified the bytes the program is reading; - requires the public key to be one of the vault's attesters and not already counted;
- rebuilds the expected message from its own fields, with the code recomputed on chain (a SHA-256 syscall and its own base58 and base32 encoders), and requires the signed message to start with exactly those bytes;
- parses the epoch and the timestamp strictly (digits only, no sign, no leading zero) and requires the epoch to match and the timestamp to be at most 15 minutes old and at most 60 seconds ahead of the cluster clock.
With at least threshold such signatures the wallet is bound. The tests cover a wrong platform name, an extra byte, a leading zero, an upper-cased message, another coin, another account, another epoch, and the freshness limits to the millisecond.
Time
The program uses the cluster clock in seconds. Due rotations and expiries are applied lazily, at the next instruction, and a rotation that comes due raises the epoch before anything else runs. A transaction that is refused leaves no trace, including the tick it would have applied. For that reason a client must build attestations against the effective state, which effectiveState(state, now) in the client computes.
Money
Fees arrive as plain SOL transfers to the vault address, which is exactly what pump.fun's distribution does. crank treats everything above the rent reserve and the amount already pending as new fees. The invariant accrued = pending + paid to streamer + paid to launcher + burned holds after every instruction, and the vault always holds exactly the rent reserve plus pending.
Solana refuses to credit a system account that holds nothing with less than the rent minimum for an empty account (890,880 lamports). A claim to a brand-new wallet with less than that pending fails, changes nothing, and works once the wallet holds any SOL or enough has accrued. The same applies to a burn share sent to an empty incinerator account. The test suite includes this case.
Errors
| Code | Name | Meaning |
|---|---|---|
| 1 | bad_config | Invalid configuration at initialize. |
| 2 | bad_account | A wrong account was passed (vault, launcher, incinerator or wallet). |
| 3 | not_enough_proof | Fewer valid attesters than the threshold. |
| 4 | already_bound | A wallet is already bound. |
| 5 | not_bound | Nothing to rotate. |
| 6 | rotation_pending | A rotation is already waiting. |
| 7 | same_wallet | That wallet is already bound. |
| 8 | no_rotation | Nothing to cancel. |
| 9 | not_signer | A signature is missing or from the wrong wallet. |
| 10 | malformed | Instruction data cannot be read. |
| 11 | already_initialized | A vault already exists at that address. |
| 12 | math | An arithmetic overflow was caught. |
Cost
Measured on the local runtime, in compute units (a transaction may use 200,000 by default and up to 1,400,000):
| crank | claim | rotate | cancel_rotation |
|---|---|---|---|
| 2479 | 20380 | 19597 | 936 |
Build and test
# toolchain on Windows, no admin rights: _tools/install-solana-toolchain.ps1
cd program && cargo build-sbf # target/deploy/upstream_vault.so
node _tools/chain-up.mjs # local Solana runtime with the program installed
node --test tests-chain/vault.test.mjs
SEEDS=60 STEPS=45 node --test tests-chain/differential.test.mjs
Deploying
None of this has been run for a real cluster. The steps are the standard ones:
solana program deploy program/target/deploy/upstream_vault.so --program-id <keypair.json> --url devnet
# when the build is audited and you are sure, remove the ability to change it, forever:
solana program set-upgrade-authority <PROGRAM_ID> --final --url devnet
A program whose upgrade authority has been removed cannot be changed by anyone, including its author. That is what "no admin key" means on chain, and it is the step to take only after an audit.