The vault program shows how to use Quasar to custody native SOL in a per-user program-derived address. A user deposits any amount of lamports into their personal vault PDA via a System Program CPI, and later withdraws them with direct lamport manipulation — no token mints, no SPL accounts, just raw SOL. This example is deliberately minimal so you can focus on the PDA address constraint, the two distinct CPI strategies (System Program transfer vs. directDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/blueshift-gg/quasar/llms.txt
Use this file to discover all available pages before exploring further.
set_lamports), and how the Quasar accounts context wires everything together.
Program Entry Point
PDA Design
The vault derives its address from two seeds: the literal byte string"vault" and the user’s public key. This guarantees a unique, deterministic vault address per user per program.
#[derive(Seeds)] generates a VaultPda::seeds(user: &Address) function that returns the canonical program-derived address. This function is used in the address = constraint on the vault account to verify at validation time that the caller provided the correct PDA, not an arbitrary account.
On the client side:
Instruction 1: Deposit
The deposit instruction uses the System Program’stransfer instruction to move lamports from the user’s wallet into the vault PDA.
Accounts
| Account | Type | Constraint | Purpose |
|---|---|---|---|
user | Signer | mut | Signs the transaction and funds the transfer |
vault | UncheckedAccount | mut, address = VaultPda::seeds(user.address()) | Destination PDA; no discriminator check needed because it holds plain SOL |
system_program | Program<SystemProgram> | — | Required for the CPI |
UncheckedAccount is the correct type here because a fresh vault PDA has no data or owner set until the first deposit arrives — it is just an address with lamports. Quasar’s address = VaultPda::seeds(user.address()) constraint replaces the need for an owner or discriminator check by verifying the PDA derivation directly.
Implementation
transfer CPI requires the sender (user) to be a signer, which Quasar has already verified through the Signer type. The call is a single method chain that compiles down to a direct sol_invoke_signed syscall with no intermediate allocations.
Client Instruction
Instruction 2: Withdraw
The withdraw instruction does not use a CPI — it mutates thevault and user lamport counters directly. This is possible because the vault PDA is owned by the program, so the program has permission to reduce its lamport balance. The System Program only allows the account owner to debit; since this program is the owner of the vault PDA, it can write lamports freely.
Accounts
system_program is not required here because no CPI is issued. The address = constraint still ensures only the genuine vault PDA for this user can be passed.
Implementation
to_account_view() returns a thin pointer into the SVM input buffer for that account. set_lamports writes through that pointer directly — no serialization, no copies, no CPI overhead.
Client Instruction
Vault Lifecycle
Derive the vault address off-chain
Before building any instruction the client computes the vault PDA:This address is passed as the
vault account in both DepositInstruction and WithdrawInstruction. The program re-derives it on-chain via the address = VaultPda::seeds(user.address()) constraint and rejects any mismatched address.Deposit SOL
Build and send a
DepositInstruction. The System Program transfers amount lamports from the user’s wallet to the vault PDA. The vault account need not exist yet — the System Program will create it with zero data and assign ownership to the vault program.Test Suite
Quasar ships an embedded SVM test runner. The vault tests demonstrate the expected lamport balances after each operation:Extending the Vault
Add a withdrawal fee
In
withdraw, deduct a basis-point fee before crediting the user and send it to a treasury account. Add the treasury as an UncheckedAccount in the Withdraw struct and call set_lamports on it.Multiple vaults per user
Add a
u64 nonce seed to VaultPda — #[seeds(b"vault", user: Address, id: u64)] — so each user can open many independent vaults identified by a numeric ID.Timed unlock
Store a
lock_until: i64 Unix timestamp in a #[account] struct co-located at the vault PDA. Check Clock::get()?.unix_timestamp >= lock_until at the top of withdraw and return an error if the vault is still locked.Multi-owner vault
Replace
user: Address in the PDA seeds with a multisig config address and require m-of-n signatures in the Withdraw accounts struct using remaining_accounts.The Quasar repository also includes a multisig example at
examples/multisig/ that demonstrates CtxWithRemaining, dynamic signer parsing with remaining_accounts().parse::<Signer, 10>(), and a Vec<Address, 10> field capped at compile time. It is a good next step after the vault if you need multi-party authorization patterns.