A PDA-based SOL vault system on Solana. Manage the full lifecycle: create vaults, deposit SOL, assign claim amounts, process claims, withdraw leftovers, and clean up accounts.
Built with Anchor framework.
Warning
This software has not undergone a professional security audit. Use it at your own risk. See the security policy for vulnerability reporting.
- Upgrade-authority-controlled creator configuration
- Separate vault creator and per-vault operational authority
- PDA-based vault and claim-record accounts
- SOL deposits, authority-managed payouts, and withdrawals
- Explicit open and closed lifecycle states
- Rent recovery through vault and claim-record cleanup
- Anchor client example and
@solana/kitPDA helpers
- Anchor CLI 1.1.2
- Solana CLI 3.1.14
- Rust 1.89.0 and Cargo
- Bun (for the integration test)
Windows is supported through WSL 2. Install the requirements and run all build, test, and deployment commands from a WSL terminal.
The integration test runs against a disposable local validator. It generates a temporary program ID and wallets, leaving the placeholders in this repository unchanged.
bun install
cd tests
bun run testThe test requires Anchor CLI, Solana CLI, solana-test-validator, and Bun.
Compiled dependencies are cached under the ignored target/local-test-cache/
directory; temporary IDs, keypairs, and validator data are still deleted after
every run. To retain a failed run's build and validator logs for diagnosis, run
VAULT_TEST_KEEP_ARTIFACTS=1 bun run test.
Vault — Holds deposited SOL for a single vault instance.
| Field | Type | Description |
|---|---|---|
authority |
Pubkey |
Wallet that controls the vault |
vault_id |
[u8; 32] |
Unique fixed-size vault identifier |
status |
VaultStatus |
Current lifecycle state |
total_deposited |
u64 |
Cumulative lamports deposited |
total_claimed |
u64 |
Cumulative lamports claimed |
total_withdrawn |
u64 |
Withdrawals counted against tracked deposits |
bump |
u8 |
PDA bump seed |
Space: 98 bytes (8 + 32 + 32 + 1 + 8 + 8 + 8 + 1)
ClaimRecord — Tracks a payout that the vault authority may process for a claimant. It does not reserve or escrow SOL.
| Field | Type | Description |
|---|---|---|
vault |
Pubkey |
Associated vault address |
authority |
Pubkey |
Vault authority at time of creation |
claimant |
Pubkey |
Wallet that gets the payout |
amount |
u64 |
Authority-managed payout (lamports) |
claimed_at |
i64 |
Unix timestamp of claim (0 if unclaimed) |
bump |
u8 |
PDA bump seed |
Space: 121 bytes (8 + 32 + 32 + 32 + 8 + 8 + 1)
VaultConfig — Stores the signer allowed to create vaults. It is created and updated only by the program upgrade authority.
| Field | Type | Description |
|---|---|---|
vault_creator |
Pubkey |
Signer allowed to create vaults |
bump |
u8 |
PDA bump seed |
Space: 41 bytes (8 + 32 + 1)
| Account | Seeds |
|---|---|
Vault |
["vault", vault_id] |
ClaimRecord |
["claim", vault_pubkey, claimant_pubkey] |
VaultConfig |
["vault_config"] |
Open ──deposit──► Open ──close_vault──► Closed
│
set_claimable / claim
withdraw / withdraw_all
│
cleanup_vault
| Variant | Description |
|---|---|
Open |
Vault is active — deposits and close are allowed |
Closed |
Locked — claims, withdrawals, and cleanup are now enabled |
Creates or updates the singleton VaultConfig PDA. Only the program upgrade
authority can call these instructions. vault_creator must not be the default
public key.
Creates a vault using the configured vault_creator as payer and signer, while
recording the supplied vault_authority as the vault's operational authority.
This separates daily vault operations from the program upgrade authority.
- Signer:
vault_creator(must matchVaultConfig.vault_creator) - Accounts: vault (init), vault_config, vault_creator, system_program
- Validations:
vault_idandvault_authoritymust not be default values - Status after:
Open
Transfers control of one vault to a new non-default authority. Only the current vault authority can call it. Existing unclaimed claim records retain their recorded authority for cleanup; this instruction does not rewrite them.
Transfers SOL from the authority into the vault via CPI to the system program.
- Signer:
authority(vault authority) - Accounts: vault (mut), authority (mut), system_program
- Validations:
amount > 0; vault must beOpen
Locks the vault, preventing further deposits. Enables claims, withdrawals, and cleanup.
- Signer:
authority(vault authority) - Accounts: vault (mut), authority
- Validations: vault status must be
Open - Status after:
Closed
Creates a ClaimRecord PDA for a claimant, recording an intended payout. No SOL
is transferred or reserved. The authority can still withdraw funds before the
payout is processed, so a claim record is not a funding guarantee.
- Signer:
authority(vault authority) - Accounts: vault (mut), claimant (unchecked), claim_record (init), authority (mut), system_program
- Validations:
amount > 0; vault must beClosed
Pays out a claimant. The authority—not the claimant—must sign, and SOL transfers directly from the vault to the recipient. Claimants cannot redeem records independently.
- Signer:
authority(vault authority) - Accounts: vault (mut), claim_record (mut), authority, recipient (mut)
- Validations: vault
Closed; claim_record matches vault + recipient; not already claimed; sufficient funds
Authority withdraws a specific amount of SOL from the vault.
- Signer:
authority(vault authority) - Accounts: vault (mut), authority, destination (mut)
- Validations:
amount > 0; vaultClosed; amount ≤ available lamports (above rent-exempt minimum)
Authority withdraws all available SOL (above rent-exempt minimum) from the vault.
- Signer:
authority(vault authority) - Accounts: vault (mut), authority, destination (mut)
- Validations: vault
Closed; available > 0
Closes the vault account and gets rent back. The tracked deposited balance must
be settled first (total_deposited == total_claimed + total_withdrawn). Claim
records do not reserve funds, so unclaimed records do not block vault cleanup.
Any unsolicited SOL dust is drained before closing.
- Signer:
authority(vault authority) - Accounts: vault (mut, close → destination), authority, destination (mut)
- Validations: vault
Closed; accounting needs to balance
Closes a claim record and gets rent back. Two ways:
- Claimed records: The claimant signs to get their rent back
- Unclaimed records: The authority signs to clean up abandoned records
Vault-independent — works even after the vault is closed.
- Signer:
signer(claimant or authority) - Accounts: claim_record (mut, close → signer), signer (mut)
| Code | Message |
|---|---|
InvalidVaultId |
Invalid vault ID |
InvalidAmount |
Invalid amount |
InvalidVaultStatus |
Vault is not in the correct status for this operation |
InsufficientFunds |
Insufficient funds in vault |
Overflow |
Arithmetic overflow |
AlreadyClaimed |
Payout already claimed |
InvalidClaimRecord |
Claim record does not match expected claimant |
NothingToClaim |
Nothing to claim |
NothingToWithdraw |
Nothing to withdraw |
Unauthorized |
Unauthorized |
VaultAccountingMismatch |
Vault accounting mismatch |
InvalidAuthority |
Invalid authority |
Please report vulnerabilities privately by following the security policy.
Licensed under the MIT License.