Skip to content

CodingGeoff/forever_vault_solana

Repository files navigation

ForeverMoney: Permissioned Liquidity Vault (Solana V2)

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.


Table of Contents


Overview

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:

  1. The Protocol Admin sets a maximum liquidity ceiling
  2. The Miner can deploy up to that ceiling in any configuration
  3. The Miner cannot withdraw funds — the program has no withdraw instruction
  4. All state changes are atomic and verifiable on-chain

Key Properties

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

System Architecture

Role-Based Access Control (RBAC)

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                      │ │
│  └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

PDA Derivation Hierarchy

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

State Accounts

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)

PositionState

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)


Security Model

The No-Withdrawal Guarantee

Question: How can we trust the Miner with capital deployment if they can steal funds?

Answer: The Miner physically cannot steal funds because:

  1. 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)
  2. 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
  3. 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)]

Atomic Ceiling Enforcement

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:

  1. Sequential Processing: Solana validators process transactions in order, even when submitted simultaneously
  2. State Locking: During transaction execution, the account state is locked
  3. 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.

Role Separation Benefits

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

Program Instructions

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

Instruction Details

initialize_vault(ceiling_l: u128, miner: Pubkey)

Creates a new vault with the caller as the protocol admin.

Accounts:

  • vault_state (writable, PDA): New vault account
  • admin (signer, writable): Becomes protocol_admin
  • system_program: Required for account creation

PDA Seeds: ["vault", admin.key()]


set_ceiling(new_ceiling_l: u128)

Updates the vault's liquidity ceiling. Can be set higher or lower than current deployment.

Accounts:

  • vault_state (writable): Vault to update
  • protocol_admin (signer): Must match stored admin

Note: If new ceiling < total_deployed_l, existing positions remain active but no new positions can be opened.


force_close_position()

Admin override to close any position, useful when the Miner is unavailable or for governance decisions.

Accounts:

  • vault_state (writable): Parent vault
  • position (writable): Position to close
  • protocol_admin (signer): Must match stored admin
  • owner_vault: Vault reference for ownership verification

open_position(tick_lower: i32, tick_upper: i32, liquidity: u128)

Creates a new concentrated liquidity position. Fails if total_deployed_l + liquidity > ceiling_l.

Accounts:

  • vault_state (writable): Vault to deploy into
  • position (writable, PDA): New position account
  • miner (signer, writable): Must match stored miner
  • system_program: Required for account creation

PDA Seeds: ["position", vault.key(), position_id.to_le_bytes()]

Validations:

  • tick_lower < tick_upper
  • liquidity > 0
  • total_deployed_l + liquidity <= ceiling_l

close_position()

Closes an active position, releasing its liquidity from the vault's total.

Accounts:

  • vault_state (writable): Parent vault
  • position (writable): Position to close
  • miner (signer): Must match stored miner
  • owner_vault: Vault reference for ownership verification

Validations:

  • Position must be active (is_active == true)
  • Position must belong to the vault

Local Development & Testing

Prerequisites

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

Quick Start

# 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-validator

Test Suite Overview

The 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

Adversarial Test Cases (Required)

Three critical adversarial tests verify the security model:

  1. Miner → set_ceiling: Verifies Miner cannot modify vault configuration
  2. Miner → exceed ceiling: Verifies atomic ceiling enforcement
  3. Admin → open_position: Verifies role separation (Admin cannot bypass Miner role)

Integration with Squads V4

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:

  1. Delegates LP-management rights to a trusted Miner
  2. Maintains multisig governance for admin operations
  3. 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 Reference

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

License

MIT License


Contributing

Contributions are welcome! Please ensure all tests pass before submitting a pull request:

anchor test

For security concerns, please open an issue or contact the maintainers directly.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors