Anchor/Rust smart contract for rate-limited escrow vaults on Solana.
See the root README for a full project overview and architecture diagrams.
backend/
├── programs/flowvault/src/
│ ├── lib.rs # Program entrypoint + instruction dispatcher
│ ├── errors.rs # FlowVaultError enum (6 error codes)
│ ├── state/
│ │ ├── mod.rs # Re-exports
│ │ └── escrow.rs # EscrowState account struct (11 fields)
│ └── instructions/
│ ├── mod.rs # Re-exports
│ ├── initialize.rs # Create escrow PDA + vault PDA, deposit SOL
│ ├── withdraw.rs # Rate-limited withdrawal with window reset logic
│ └── cancel.rs # Drain vault + close escrow, return rent to sender
├── tests/
│ └── flowvault.ts # 12 integration tests (Mocha + Chai)
├── scripts/
│ └── deploy-devnet.sh # Build + deploy + copy IDL to frontend
├── migrations/
│ └── deploy.ts # Anchor migration entrypoint
├── Anchor.toml # Anchor workspace config
├── Cargo.toml # Rust workspace
├── package.json # JS test dependencies (bun-managed)
└── rust-toolchain.toml # Pinned Rust 1.89
| Tool | Version | Install |
|---|---|---|
| Rust | 1.89+ | rustup.rs |
| Solana CLI | 2.x | docs.solanalabs.com |
| Anchor CLI | 0.32.1 | anchor-lang.com |
| Node.js | 20+ | nodejs.org |
| Bun | latest | bun.sh |
Verify:
rustc --version # rustc 1.89.x
solana --version # solana-cli 2.x.x
anchor --version # anchor-cli 0.32.1
bun --version # 1.xAll commands run from the backend/ directory.
cd backend
# Install JS test dependencies
bun install
# Build the Solana program (generates IDL + keypair)
anchor build# Build the program
anchor build
# Run all 12 integration tests (spins up a local validator automatically)
anchor test
# Deploy to local validator (must be running solana-test-validator)
anchor deploy --provider.cluster localnet
# Deploy to devnet
anchor deploy --provider.cluster devnet
# Full devnet deployment (build + deploy + copy IDL to frontend)
bash scripts/deploy-devnet.sh
# Lint JS/TS files
bun run lint
# Format JS/TS files
bun run lint:fixYou need two terminals to run the full stack.
Terminal 1 — Local validator
solana-test-validatorTerminal 2 — Deploy + start frontend
# From backend/
anchor deploy --provider.cluster localnet
# From frontend/
cd ../frontend
bun devOpen http://localhost:3000 and connect a wallet.
Tip: Get free devnet SOL with
solana airdrop 2.
12 integration tests covering all instructions, validation, access control, and edge cases.
anchor test| # | Test | What It Verifies |
|---|---|---|
| 1 | Initialize — correct state | Escrow created with all fields set correctly |
| 2 | Initialize — reject high spend limit | spend_limit > amount is rejected |
| 3 | Initialize — reject zero amount | amount = 0 is rejected |
| 4 | Withdraw — within limit | Receiver withdraws within window spend limit |
| 5 | Withdraw — exceed window limit | Exceeding remaining window allowance is rejected |
| 6 | Withdraw — window reset | Full limit is available again after window expires |
| 7 | Withdraw — wrong signer | Non-receiver wallet cannot withdraw |
| 8 | Cancel — reclaim funds | Sender closes escrow and gets all SOL back |
| 9 | Cancel — wrong signer | Non-sender wallet cannot cancel |
| 10 | Withdraw after cancel | Closed escrow account cannot be used |
| 11 | Vault fully drained | Withdrawal fails with InsufficientFunds |
| 12 | Multiple escrows | Different seeds create independent vaults |
| Network | Address |
|---|---|
| Devnet | 5eyT6xJEG3Qen3QD59zNEdiE4vkYgJdnNtwjSQYCnbyb |
| Mainnet | Not yet deployed |
EscrowState — stores configuration and tracking data for one escrow.
| Field | Type | Description |
|---|---|---|
sender |
Pubkey |
Wallet that created and funded the escrow |
receiver |
Pubkey |
Wallet authorized to withdraw |
seed |
u64 |
Unique seed (enables multiple escrows per pair) |
total_deposited |
u64 |
Lamports deposited at creation |
spend_limit |
u64 |
Max lamports withdrawable per window |
window_duration |
i64 |
Window length in seconds |
window_start |
i64 |
Unix timestamp of current window start |
withdrawn_in_window |
u64 |
Lamports withdrawn in current window |
total_withdrawn |
u64 |
Lifetime total lamports withdrawn |
bump |
u8 |
Escrow PDA bump seed |
vault_bump |
u8 |
Vault PDA bump seed |
| Account | Seeds | Derivation |
|---|---|---|
| Escrow State | "escrow" + sender + receiver + seed |
One PDA per sender/receiver/seed combination |
| Vault | "vault" + escrow_state |
One vault per escrow, derived from escrow PDA |
| Code | Name | Message |
|---|---|---|
| 6000 | InvalidAmount |
Amount must be greater than zero |
| 6001 | InvalidSpendLimit |
Spend limit must be greater than zero and at most the deposit amount |
| 6002 | InvalidWindowDuration |
Window duration must be greater than zero |
| 6003 | SpendLimitExceeded |
Withdrawal would exceed the spend limit for this window |
| 6004 | InsufficientFunds |
Vault has insufficient funds for this withdrawal |
| 6005 | MathOverflow |
Arithmetic overflow |
| Pattern | Implementation |
|---|---|
| Checks-Effects-Interactions | State is validated and updated before any SOL transfer via CPI |
| PDA-Enforced Authorization | Vault funds can only move through program-signed CPI; no private keys involved |
has_one Constraints |
withdraw enforces has_one = receiver; cancel enforces has_one = sender |
| Clean Account Closure | close = sender returns rent and zeroes account data on cancellation |
| Overflow Protection | All arithmetic uses checked_add with explicit MathOverflow errors |
| No Reinitialization | Anchor's init constraint prevents double-initialization of escrow accounts |