upstream

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:

OffsetSizeField
01version (1)
11bump
21platform (0 github, 1 youtube, 2 twitch, 3 kick)
31on expiry (0 launcher, 1 burn)
4, 51, 1threshold, number of attesters
6, 8, 102 eachstreamer, launcher, burn shares in basis points
12, 131, 1length of the account id, length of the mint in base58
164epoch
24, 328 eachexpiry and rotation delay, seconds
40, 48, 56, 648 eachcreated at, window start, bound at, rotation effective at (0 = none)
72 to 1048 eachaccrued, pending, paid to streamer, paid to launcher, burned
112, 144, 176, 20832 eachmint, launcher, bound wallet, pending rotation wallet (all zero = none)
24064account id, ASCII
30444mint in base58, ASCII
352160up to five attester public keys

decodeVault in src/chain/client.mjs reads exactly this layout.

Instructions

TagNameAccountsWhat it does
0initializepayer (signer), vault, launcher (signer), system programValidates the configuration (the same rules as new Vault() in JS) and creates the vault.
1crankvault, launcher, incinerator, bound walletApplies due rotations and expiries, then measures the lamports that arrived since the last call and splits them. Anyone can call it.
2claimvault, launcher, incinerator, wallet to bind, instructions sysvarNeeds Ed25519 instructions just before it. Pays everything pending to the wallet and binds it.
3rotatevault, launcher, incinerator, new wallet, instructions sysvarSame proof, for the new wallet; schedules the change after the delay.
4cancel_rotationvault, 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:

  1. 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;
  2. requires the public key to be one of the vault's attesters and not already counted;
  3. 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;
  4. 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.

A rent limit worth knowing

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

CodeNameMeaning
1bad_configInvalid configuration at initialize.
2bad_accountA wrong account was passed (vault, launcher, incinerator or wallet).
3not_enough_proofFewer valid attesters than the threshold.
4already_boundA wallet is already bound.
5not_boundNothing to rotate.
6rotation_pendingA rotation is already waiting.
7same_walletThat wallet is already bound.
8no_rotationNothing to cancel.
9not_signerA signature is missing or from the wrong wallet.
10malformedInstruction data cannot be read.
11already_initializedA vault already exists at that address.
12mathAn 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):

crankclaimrotatecancel_rotation
24792038019597936

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.