A production-ready Solana smart contract implementing a permissioned liquidity management system that enables protocols to safely delegate capital deployment to trusted operators while maintaining strict security boundaries.
This system mirrors the "Gnosis Safe + Zodiac Roles" governance pattern from Ethereum, adapted for Solana's account model using Program Derived Addresses (PDAs). It provides a trust-minimized framework where a Protocol Admin can grant limited operational rights to a Miner without risking fund theft.
- Overview
- System Architecture
- Security Model
- Program Instructions
- Local Development & Testing
- Integration with Squads V4
- Error Reference
Forever Vault Solana solves a critical DeFi infrastructure problem: How can a protocol safely delegate liquidity management to an operator without risking fund theft?
Traditional solutions require either:
- Full custody transfer (operator can steal funds)
- Multi-signature approval for every operation (too slow for active LP management)
This program introduces a ceiling-based permission model where:
- The Protocol Admin sets a maximum liquidity ceiling
- The Miner can deploy up to that ceiling in any configuration
- The Miner cannot withdraw funds — the program has no withdraw instruction
- All state changes are atomic and verifiable on-chain
| Property | Implementation |
|---|---|
| No-Withdraw Guarantee | Program lacks any transfer-out instruction |
| Atomic Ceiling Enforcement | Checked before state modification in open_position |
| Role Separation | Admin controls config; Miner controls positions |
| PDA Authority | All funds controlled by program-derived addresses |
| Emergency Override | Admin can force-close positions if Miner is unresponsive |
The system implements a two-tier permission structure:
┌─────────────────────────────────────────────────────────────────┐
│ PROTOCOL ADMIN │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ • Ultimate authority over vault configuration │ │
│ │ • Can set/modify liquidity ceiling │ │
│ │ • Can force-close any position (emergency override) │ │
│ │ • Typically a multisig (Squads V4) in production │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ delegates to │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ MINER │ │
│ │ • Can open liquidity positions within ceiling limit │ │
│ │ • Can close their own positions │ │
│ │ • CANNOT withdraw funds (no instruction exists) │ │
│ │ • CANNOT modify ceiling │ │
│ │ • CANNOT access other vaults' positions │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
The system uses a hierarchical PDA structure for deterministic addressing and ownership verification:
VaultState PDA
├── Seeds: ["vault", admin_pubkey]
├── Stores: protocol_admin, miner, ceiling_l, total_deployed_l, position_count
│
└── PositionState PDA (multiple per vault)
├── Seeds: ["position", vault_pubkey, position_id]
├── Stores: id, owner_vault, tick_lower, tick_upper, liquidity, is_active
└── Linked via owner_vault field to parent VaultState
| Field | Type | Size | Description |
|---|---|---|---|
protocol_admin |
Pubkey | 32 bytes | Ultimate authority for vault config |
miner |
Pubkey | 32 bytes | Designated operator for position management |
ceiling_l |
u128 | 16 bytes | Maximum deployable liquidity |
total_deployed_l |
u128 | 16 bytes | Current sum of active position liquidity |
position_count |
u64 | 8 bytes | Counter for unique position IDs |
Total Size: 112 bytes (including 8-byte discriminator)
| Field | Type | Size | Description |
|---|---|---|---|
id |
u64 | 8 bytes | Unique position identifier |
owner_vault |
Pubkey | 32 bytes | Reference to parent VaultState |
tick_lower |
i32 | 4 bytes | Lower price bound (concentrated liquidity) |
tick_upper |
i32 | 4 bytes | Upper price bound (concentrated liquidity) |
liquidity |
u128 | 16 bytes | Amount of liquidity deployed |
is_active |
bool | 1 byte | Position status flag |
Total Size: 73 bytes (including 8-byte discriminator)
Question: How can we trust the Miner with capital deployment if they can steal funds?
Answer: The Miner physically cannot steal funds because:
-
No Withdraw Instruction Exists
The program deliberately omits any instruction that transfers tokens out of the vault. The only ways funds can leave are:
- Administrative action (not implemented in this version)
- External protocol integration via CPI (requires PDA signature)
-
PDA-Controlled Authority
All positions are owned by PDAs, not user keypairs. The Miner can only:
- Create position state records
- Update position state flags
- They cannot sign for token transfers because only the program can sign for its PDAs
-
Instruction Whitelisting
Every instruction validates the caller's role:
// Only protocol_admin can call set_ceiling #[account(has_one = protocol_admin @ VaultError::UnauthorizedAdmin)] // Only miner can call open_position #[account(has_one = miner @ VaultError::UnauthorizedMiner)]
Question: Can a Miner bypass the ceiling by spamming concurrent transactions?
Answer: No. Solana's sequential transaction processing prevents this.
The ceiling check is performed atomically before any state modification:
pub fn open_position(ctx: Context<OpenPosition>, ..., liquidity: u128) -> Result<()> {
// CRITICAL: Ceiling check BEFORE state modification
let new_total = vault.total_deployed_l.checked_add(liquidity)?;
require!(new_total <= vault.ceiling_l, VaultError::CeilingExceeded);
// Only after validation, update state
vault.total_deployed_l = new_total;
// ... create position
}Why this works on Solana:
- Sequential Processing: Solana validators process transactions in order, even when submitted simultaneously
- State Locking: During transaction execution, the account state is locked
- Atomic Rollback: If the ceiling check fails, the entire transaction reverts with no state changes
Attack Scenario (and why it fails):
Time Transaction 1 (700 liquidity) Transaction 2 (700 liquidity)
────────────────────────────────────────────────────────────────────
T0 Reads total_deployed_l = 500
T1 Reads total_deployed_l = 500
T2 Check: 500 + 700 = 1200 ≤ 2000 ✓
T3 Check: 500 + 700 = 1200 ≤ 2000 ✓
T4 Writes total_deployed_l = 1200 ❌ CONFLICT - Account locked
T5 Transaction re-runs with fresh state
T6 Check: 1200 + 700 = 1900 ≤ 2000 ✓
T7 Writes total_deployed_l = 1900
The second transaction re-reads the state after the first commits, ensuring the ceiling is always enforced correctly.
| Scenario | Admin Can | Miner Can | Result |
|---|---|---|---|
| Increase deployment capacity | ✓ set_ceiling |
✗ | Admin controls risk exposure |
| Emergency exit position | ✓ force_close_position |
✓ close_position |
Both parties can reduce exposure |
| Daily LP rebalancing | ✗ | ✓ open_position / close_position |
Miner operates autonomously |
| Modify Miner address | Via vault reinitialization | ✗ | Admin controls delegation |
| Steal funds | ✗ No withdraw instruction | ✗ No withdraw instruction | Neither party can steal |
| Instruction | Authority | Description |
|---|---|---|
initialize_vault |
Admin | Creates a new vault PDA with specified ceiling and designated miner |
set_ceiling |
Admin | Updates the maximum deployable liquidity limit |
force_close_position |
Admin | Emergency override to close any position |
open_position |
Miner | Deploys liquidity within the ceiling limit |
close_position |
Miner | Closes an active position, freeing ceiling capacity |
Creates a new vault with the caller as the protocol admin.
Accounts:
vault_state(writable, PDA): New vault accountadmin(signer, writable): Becomes protocol_adminsystem_program: Required for account creation
PDA Seeds: ["vault", admin.key()]
Updates the vault's liquidity ceiling. Can be set higher or lower than current deployment.
Accounts:
vault_state(writable): Vault to updateprotocol_admin(signer): Must match stored admin
Note: If new ceiling < total_deployed_l, existing positions remain active but no new positions can be opened.
Admin override to close any position, useful when the Miner is unavailable or for governance decisions.
Accounts:
vault_state(writable): Parent vaultposition(writable): Position to closeprotocol_admin(signer): Must match stored adminowner_vault: Vault reference for ownership verification
Creates a new concentrated liquidity position. Fails if total_deployed_l + liquidity > ceiling_l.
Accounts:
vault_state(writable): Vault to deploy intoposition(writable, PDA): New position accountminer(signer, writable): Must match stored minersystem_program: Required for account creation
PDA Seeds: ["position", vault.key(), position_id.to_le_bytes()]
Validations:
tick_lower < tick_upperliquidity > 0total_deployed_l + liquidity <= ceiling_l
Closes an active position, releasing its liquidity from the vault's total.
Accounts:
vault_state(writable): Parent vaultposition(writable): Position to closeminer(signer): Must match stored minerowner_vault: Vault reference for ownership verification
Validations:
- Position must be active (
is_active == true) - Position must belong to the vault
| Tool | Version | Installation |
|---|---|---|
| Rust | 1.75+ | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| Solana CLI | 1.17+ | sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)" |
| Anchor | 0.30+ | avm install latest && avm use latest |
| Node.js | 18+ | nvm install 18 && nvm use 18 |
# Clone the repository
git clone https://github.com/CodingGeoff/forever-vault-solana.git
cd forever-vault-solana/forever_vault_solana
# Install dependencies
npm install
# Build the program
anchor build
# Generate program keypair (first build will fail without this)
solana-keygen new -o target/deploy/forever_vault_solana-keypair.json --force
# Update program ID in lib.rs and rebuild
# Copy the new program ID from: solana address -k target/deploy/forever_vault_solana-keypair.json
anchor build
# Run tests (with local validator)
anchor test
# OR run tests with existing validator
anchor test --skip-local-validatorThe test suite includes comprehensive coverage across these categories:
forever_vault_solana
├── Vault Setup
│ ├── ✓ Initialize vault with correct parameters
│ └── ✓ Update vault ceiling
├── Position Management
│ ├── ✓ Open position within ceiling
│ ├── ✓ Open second position
│ ├── ✓ Close position and free liquidity
│ └── ✓ Open position after close
├── Adversarial Tests (Security Boundary Testing)
│ ├── ✓ REJECTS Miner calling set_ceiling
│ ├── ✓ REJECTS Miner exceeding ceiling
│ ├── ✓ REJECTS Admin calling open_position
│ ├── ✓ REJECTS Unauthorized user
│ └── ✓ REJECTS Double-close of position
├── Input Validation
│ ├── ✓ REJECTS invalid tick range
│ └── ✓ REJECTS zero liquidity
├── Admin Override
│ ├── ✓ Force close position by admin
│ └── ✓ REJECTS non-admin force close
└── State Consistency
├── ✓ Final vault state verification
└── ✓ All position states verified
Three critical adversarial tests verify the security model:
- Miner → set_ceiling: Verifies Miner cannot modify vault configuration
- Miner → exceed ceiling: Verifies atomic ceiling enforcement
- Admin → open_position: Verifies role separation (Admin cannot bypass Miner role)
In a production environment, the protocol_admin should be a Squads V4 multisig rather than an individual keypair. This program acts as a "Role Module" that:
- Delegates LP-management rights to a trusted Miner
- Maintains multisig governance for admin operations
- Provides emergency controls if the Miner becomes unresponsive
Recommended setup:
Squads V4 Multisig (3/5 threshold)
│
└──► VaultState.protocol_admin
│
└──► Can set_ceiling, force_close_position
│
└──► Miner (hot keypair)
│
└──► Can open_position, close_position
| Error Code | Name | Description |
|---|---|---|
| 6000 | UnauthorizedAdmin |
Signer is not the authorized protocol admin |
| 6001 | UnauthorizedMiner |
Signer is not the authorized miner for this vault |
| 6002 | CeilingExceeded |
Deployment would exceed the vault's liquidity ceiling |
| 6003 | MathOverflow |
Arithmetic operation resulted in overflow |
| 6004 | PositionAlreadyClosed |
Attempted to close an already-closed position |
| 6005 | PositionInactive |
Operation requires an active position |
| 6006 | InvalidTickRange |
tick_lower must be strictly less than tick_upper |
| 6007 | ZeroLiquidity |
Cannot create position with zero liquidity |
MIT License
Contributions are welcome! Please ensure all tests pass before submitting a pull request:
anchor testFor security concerns, please open an issue or contact the maintainers directly.