A Solana program (Anchor 0.32) that holds SOL in a multi-signature vault. Only the program can move funds out; no single key can withdraw on its own.
1. Someone creates the vault
One person (the creator) sets up the vault and defines the rules:
- Who can move money: a list of N people (by wallet address), up to 5.
- How many must agree: a number M (e.g. “2 of 3”).
- Max per 24 hours: e.g. no more than X SOL per day.
- Initial 2FA code: a number used for the first withdrawal.
After that, the vault exists and has one treasury account that holds SOL. Only the program can move SOL out, and only when the rules are met.
2. Anyone can put money in
Any wallet can send SOL to the vault (deposit). No approval and no check of “who is allowed.” If you know the vault’s address, you can send SOL to it. The vault holds that SOL until a withdrawal is executed under the rules.
3. Withdrawing: propose → approve → execute
- Propose – One of the authorized signers creates a withdrawal proposal: “Send X SOL to address Y.”
- Approve – Other authorized signers approve that proposal. When at least M of the N signers have approved (e.g. 2 of 3), the proposal is ready.
- Execute – Anyone can call execute (a signer or a bot). The program only sends SOL if:
- The proposal has enough approvals (M-of-N),
- The correct 2FA (TOTP) code is sent with the execute call,
- The amount is within the 24-hour spend limit,
- The proposal has not already been executed. After a successful withdrawal, the 2FA code rotates for the next time.
So in short: create the vault (set who can withdraw and the limits) → anyone can deposit → only the fixed list of signers can propose and approve; money actually leaves only when enough have approved, the right 2FA is given, and the 24h limit is not exceeded.
-
VaultState –
[b"vault", creator]creator,signers(max 5),thresholdtotp_codespend_limit,spent_in_window,window_startrelease_nonce,proposal_countbump,authority_bump
-
ProposalState –
[b"proposal", vault, proposal_id]vault,proposer,destination,amountapprovalsexecutedproposal_id,bump
-
ProgramAuthority –
[b"authority", vault]- System-owned account (no data) that holds the SOL.
-
initialize_vault(signers, threshold, spend_limit, initial_totp)
Set up the vault: signers, threshold, spend limit, initial TOTP, and authority PDA. -
deposit(amount)
Anyone can send SOL into the vault’s authority PDA. Checksamount > 0. -
create_proposal(destination, amount)
Only a signer can call. Creates aProposalState, auto‑adds proposer toapprovals. Checks signer,amount > 0, andamount <= spend_limit. -
approve_proposal()
Only a signer can call. Adds caller toproposal.approvals. Fails if already approved or proposal executed. -
execute_withdrawal(totp_code)
Checks, in order:- Proposal not executed
approvals.len() >= thresholdtotp_code == vault.totp_code- 24h window reset and spend tracking
spent_in_window + amount <= spend_limit
If all pass, transfers lamports from authority PDA to
destination, setsexecuted = true, updatesspent_in_windowandrelease_nonce, and rotates TOTP:
totp_code = release_nonce * 7 + 111111.
NotAuthorizedSignerAlreadyApprovedInsufficientApprovalsInvalid2FACodeSpendLimitExceededProposalAlreadyExecutedInvalidThresholdInvalidAmount
All tests are in tests/multisig-vault.ts and run with:
cd multisig-vault
anchor testThey cover:
- Deposit 3 SOL into the vault.
- Create a proposal and collect 2 approvals.
- Execute withdrawal with the correct TOTP.
- Check TOTP rotation (
release_nonce == 1,totp_code == 111118). - Execute withdrawal with a wrong TOTP and see it fail.
- Program ID:
6p7fCjYTpz1V9HeyRzN2Z9CpDK6zaUPGwDHHQK1KQ4hL
Deployed with:
cd multisig-vault
anchor build
anchor deploy --provider.cluster devnet