This document analyzes the architectural decision to extract escrow functionality from the Sprint Vault program into a separate, dedicated Anchor program. The Escrow Program would serve as a centralized fund management system for both SprintVault and Bounty programs, providing a single source of truth for all financial operations.
-
Single Source of Truth
- One escrow system managing all funds
- Consistent accounting and balance tracking
- Easier auditing of fund flows
-
Reduced Code Duplication
- Both SprintVault and Bounty programs use the same escrow logic
- Maintenance is centralized
- Bug fixes apply to all consumers
-
Enhanced Security
- Specialized program focused solely on fund management
- Smaller attack surface for the critical money-handling code
- Easier to audit and formally verify
-
Flexibility for Future Programs
- New programs (Dispute, Staking, etc.) can easily integrate
- Standardized interface for all fund management
- Could support multiple token types centrally
-
Atomic Multi-Program Transactions
- Could enable complex transactions spanning multiple programs
- Better composability in the Solana ecosystem
-
Increased Complexity
- More CPIs = higher compute unit consumption
- Additional program to deploy and maintain
- More complex testing (need to test interactions)
-
Performance Overhead
- Each CPI has overhead (~2-3k compute units)
- Multiple CPIs per transaction could hit limits
- Slightly higher transaction costs
-
Dependency Risk
- If escrow program has issues, both programs fail
- Upgrades need coordination across programs
- Version compatibility concerns
-
Account Size Limitations
- Need to pass more accounts in transactions
- Could hit transaction size limits with complex operations
Given the use case of multiple programs (SprintVault, Bounty, and future Dispute program) needing escrow functionality, implementing a separate Escrow program is recommended with appropriate design considerations.
graph TD
subgraph Programs
SV[SprintVault Program]
BP[Bounty Program]
EP[Escrow Program]
DP[Dispute Program - Future]
end
subgraph Escrow Features
VA[Vault Accounts]
TM[Token Management]
WL[Withdrawal Logic]
RF[Refund Logic]
AC[Access Control]
end
SV -->|CPI| EP
BP -->|CPI| EP
DP -.->|CPI| EP
EP --> VA
EP --> TM
EP --> WL
EP --> RF
EP --> AC
style EP fill:#FFE5B4,stroke:#FF8C00,stroke-width:3px
style SV fill:#B4D4FF,stroke:#0066CC
style BP fill:#B4FFB4,stroke:#00CC00
// Primary Escrow Vault Account
#[account]
pub struct EscrowVault {
// Identity
pub vault_id: u64, // Unique identifier
pub owner_program: Pubkey, // SprintVault or Bounty program
pub owner_account: Pubkey, // Sprint PDA or Bounty PDA
// Participants
pub depositor: Pubkey, // Employer who deposits funds
pub beneficiary: Pubkey, // Freelancer/Contributor who receives
pub arbiter: Option<Pubkey>, // Optional dispute resolver
// Token Information
pub token_mint: Pubkey, // SPL token mint
pub vault_token_account: Pubkey, // Associated token account
// Amounts
pub total_amount: u64, // Total deposited amount
pub released_amount: u64, // Amount already released
pub refunded_amount: u64, // Amount refunded to depositor
pub locked_amount: u64, // Amount locked for disputes
// Release Configuration
pub release_schedule: ReleaseSchedule, // How funds are released
pub release_authority: ReleaseAuthority,// Who can trigger releases
// Status
pub status: EscrowStatus, // Current vault status
pub created_at: i64, // Creation timestamp
pub updated_at: i64, // Last update timestamp
pub expires_at: Option<i64>, // Optional expiration
// PDA
pub bump: u8, // PDA bump seed
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub enum ReleaseSchedule {
Immediate, // Release on request
Linear {
start: i64,
end: i64
}, // Time-based linear release
Milestone {
conditions: Vec<MilestoneCondition>
}, // Condition-based release
Hybrid {
linear_portion: u64, // Amount for linear release
milestone_portion: u64, // Amount for milestone release
linear_config: LinearConfig,
milestone_config: Vec<MilestoneCondition>,
}, // Combined linear + milestone
Custom {
data: Vec<u8>
}, // For future extensions
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct MilestoneCondition {
pub milestone_id: u32,
pub amount: u64,
pub required_approval: Pubkey,
pub is_completed: bool,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct LinearConfig {
pub start_time: i64,
pub end_time: i64,
pub acceleration_type: AccelerationType,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub enum AccelerationType {
Linear,
Quadratic,
Cubic,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub enum ReleaseAuthority {
Beneficiary, // Only beneficiary can withdraw
Depositor, // Only depositor can release
Either, // Either party
Both, // Requires both signatures
Program(Pubkey), // Specific program authority
Arbiter, // Arbiter controls release
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum EscrowStatus {
Initialized, // Created but not funded
Funded, // Funds deposited
Active, // Release schedule active
Paused, // Temporarily paused
Completed, // All funds distributed
Cancelled, // Cancelled and refunded
Disputed, // Under dispute
}
// Escrow Configuration Account (Program-wide settings)
#[account]
pub struct EscrowConfig {
pub authority: Pubkey, // Program authority
pub fee_basis_points: u16, // Platform fee (e.g., 100 = 1%)
pub fee_recipient: Pubkey, // Where fees go
pub min_escrow_amount: u64, // Minimum escrow size
pub max_escrow_duration: i64, // Maximum duration
pub paused: bool, // Emergency pause
pub version: u32, // Program version
pub bump: u8,
}
// Audit Log Account (Optional)
#[account]
pub struct EscrowAuditLog {
pub vault_id: u64,
pub entries: Vec<AuditEntry>,
pub bump: u8,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct AuditEntry {
pub timestamp: i64,
pub action: EscrowAction,
pub actor: Pubkey,
pub amount: Option<u64>,
pub details: String,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub enum EscrowAction {
Created,
Funded,
Released,
Refunded,
Paused,
Resumed,
Disputed,
Resolved,
Closed,
}// 1. Initialize Program Configuration (One-time setup)
pub fn initialize_config(
ctx: Context<InitializeConfig>,
fee_basis_points: u16,
min_escrow_amount: u64,
max_escrow_duration: i64,
) -> Result<()>
// 2. Create Escrow Vault
pub fn create_escrow(
ctx: Context<CreateEscrow>,
vault_id: u64,
beneficiary: Pubkey,
total_amount: u64,
release_schedule: ReleaseSchedule,
release_authority: ReleaseAuthority,
expires_at: Option<i64>,
) -> Result<()>
// 3. Deposit Funds into Escrow
pub fn deposit_funds(
ctx: Context<DepositFunds>,
amount: u64,
) -> Result<()>
// 4. Calculate Available Funds (View function)
pub fn calculate_available(
ctx: Context<CalculateAvailable>,
) -> Result<u64>
// 5. Withdraw Available Funds
pub fn withdraw_available(
ctx: Context<WithdrawAvailable>,
max_amount: Option<u64>,
) -> Result<()>
// 6. Release Milestone Funds
pub fn release_milestone(
ctx: Context<ReleaseMilestone>,
milestone_id: u32,
) -> Result<()>
// 7. Refund Funds to Depositor
pub fn refund_funds(
ctx: Context<RefundFunds>,
amount: u64,
reason: String,
) -> Result<()>
// 8. Update Release Schedule
pub fn update_release_schedule(
ctx: Context<UpdateReleaseSchedule>,
new_schedule: ReleaseSchedule,
) -> Result<()>
// 9. Pause Escrow
pub fn pause_escrow(
ctx: Context<PauseEscrow>,
reason: String,
) -> Result<()>
// 10. Resume Escrow
pub fn resume_escrow(
ctx: Context<ResumeEscrow>,
) -> Result<()>
// 11. Initiate Dispute
pub fn initiate_dispute(
ctx: Context<InitiateDispute>,
reason: String,
) -> Result<()>
// 12. Resolve Dispute
pub fn resolve_dispute(
ctx: Context<ResolveDispute>,
resolution: DisputeResolution,
) -> Result<()>
// 13. Close Escrow
pub fn close_escrow(
ctx: Context<CloseEscrow>,
) -> Result<()>
// 14. Emergency Withdraw (Admin only)
pub fn emergency_withdraw(
ctx: Context<EmergencyWithdraw>,
recipient: Pubkey,
) -> Result<()>// Escrow Vault PDA
seeds = [
b"escrow_vault",
owner_program.as_ref(),
vault_id.to_le_bytes().as_ref()
]
// Escrow Config PDA (Singleton)
seeds = [b"escrow_config"]
// Audit Log PDA
seeds = [
b"escrow_audit",
vault_id.to_le_bytes().as_ref()
]
// Vault Token Account PDA
seeds = [
b"escrow_token",
vault_id.to_le_bytes().as_ref(),
token_mint.as_ref()
]- initialize_config
- create_escrow
- deposit_funds
- calculate_available (linear release only)
- withdraw_available
- close_escrow
- Milestone-based releases
- Hybrid linear + milestone
- Custom acceleration curves
- update_release_schedule
- pause_escrow
- resume_escrow
- initiate_dispute
- resolve_dispute
- Arbiter assignment
- CPI from SprintVault
- CPI from Bounty
- Integration tests
- Performance optimization
// Each escrow is owned by the calling program
impl EscrowVault {
pub fn validate_owner_program(&self, program_id: &Pubkey) -> Result<()> {
require!(
self.owner_program == *program_id,
EscrowError::UnauthorizedProgram
);
Ok(())
}
}pub fn validate_withdrawal_authority(
escrow: &EscrowVault,
caller_program: &Pubkey,
signer: &Pubkey,
) -> Result<()> {
match escrow.release_authority {
ReleaseAuthority::Beneficiary => {
require!(signer == &escrow.beneficiary, EscrowError::Unauthorized);
},
ReleaseAuthority::Program(authorized) => {
require!(caller_program == &authorized, EscrowError::Unauthorized);
},
// ... other cases
}
Ok(())
}// Batch operations to minimize CPI overhead
pub fn batch_withdraw(
ctx: Context<BatchWithdraw>,
vault_ids: Vec<u64>,
) -> Result<Vec<u64>> {
let mut withdrawn_amounts = Vec::new();
for vault_id in vault_ids {
// Process each withdrawal
let amount = process_withdrawal(ctx, vault_id)?;
withdrawn_amounts.push(amount);
}
Ok(withdrawn_amounts)
}impl EscrowConfig {
pub fn is_compatible(&self, required_version: u32) -> Result<()> {
require!(
self.version >= required_version,
EscrowError::IncompatibleVersion
);
Ok(())
}
}- Keep escrow logic in SprintVault initially
- Get the system working end-to-end
- Identify common patterns
// Define trait for escrow operations
pub trait EscrowOperations {
fn create_vault(&self, params: VaultParams) -> Result<()>;
fn deposit(&self, amount: u64) -> Result<()>;
fn withdraw(&self, amount: u64) -> Result<()>;
fn calculate_available(&self) -> Result<u64>;
}- Deploy separate escrow program to devnet
- Test thoroughly with small amounts
- Verify all edge cases
// Support both old and new escrows during transition
pub enum EscrowType {
Legacy(LegacyEscrow),
External(ExternalEscrow),
}
impl Sprint {
pub fn migrate_to_external_escrow(&mut self) -> Result<()> {
// Migration logic
}
}- All new operations use escrow program
- Migrate remaining legacy escrows
- Deprecate embedded escrow code
| Factor | Embedded Escrow | Separate Escrow | Winner |
|---|---|---|---|
| Simplicity | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Embedded |
| Maintainability | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Separate |
| Security | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Separate |
| Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Embedded |
| Flexibility | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Separate |
| Future-proofing | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Separate |
| Composability | ⭐⭐ | ⭐⭐⭐⭐⭐ | Separate |
| Audit Simplicity | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Separate |
// Use Anchor's reentrancy protection
#[access_control(non_reentrant)]
pub fn withdraw_available(ctx: Context<WithdrawAvailable>) -> Result<()> {
// Withdrawal logic
}// Always use checked arithmetic
let available = total_amount
.checked_sub(released_amount)
.ok_or(EscrowError::ArithmeticOverflow)?;// Strict validation of all authorities
pub fn validate_authorities(ctx: &Context<WithdrawAvailable>) -> Result<()> {
let escrow = &ctx.accounts.escrow;
let signer = &ctx.accounts.signer;
// Multiple layers of validation
escrow.validate_owner_program(&ctx.program_id)?;
escrow.validate_beneficiary(signer.key)?;
escrow.validate_status(EscrowStatus::Active)?;
Ok(())
}#[account]
pub struct WithdrawalTracker {
pub last_withdrawal: i64,
pub daily_amount: u64,
pub daily_limit: u64,
}
impl WithdrawalTracker {
pub fn can_withdraw(&self, amount: u64, current_time: i64) -> Result<()> {
// Implement rate limiting logic
}
}describe("Escrow Program", () => {
describe("Create Escrow", () => {
it("Should create linear release escrow");
it("Should create milestone release escrow");
it("Should reject invalid parameters");
it("Should enforce minimum amounts");
});
describe("Withdrawals", () => {
it("Should calculate linear release correctly");
it("Should enforce release schedule");
it("Should prevent overdraw");
it("Should handle acceleration curves");
});
describe("Disputes", () => {
it("Should pause on dispute");
it("Should require arbiter for resolution");
it("Should distribute according to resolution");
});
describe("CPI Integration", () => {
it("Should accept CPI from SprintVault");
it("Should accept CPI from Bounty");
it("Should reject unauthorized programs");
});
});describe("Cross-Program Integration", () => {
it("Should handle Sprint creation with escrow");
it("Should handle Bounty milestone releases");
it("Should coordinate dispute resolution");
it("Should maintain consistency across programs");
});describe("Performance", () => {
it("Should handle batch operations efficiently");
it("Should stay within compute limits");
it("Should optimize CPI calls");
});Implement the separate Escrow program with the following priorities:
- Start Simple: Begin with core deposit/withdraw functionality
- Focus on Security: Prioritize fund safety over features
- Design for Extension: Use enums and versioning for future growth
- Monitor Performance: Track CPI costs and optimize as needed
- Plan Migration: Design with backward compatibility in mind
- Clean separation of concerns: Each program focuses on its domain
- Reusable financial infrastructure: One audited escrow for all
- Easier compliance: Centralized fund management
- Foundation for ecosystem: Enables future program integration
- Reduced audit scope: Security-critical code in one place
- Use conservative limits initially: Start with low maximums
- Implement emergency pause: Admin can halt operations if needed
- Comprehensive testing: Both unit and integration tests
- Gradual rollout: Test on devnet extensively first
- Version management: Plan for upgrades from day one
The additional complexity of CPI calls is outweighed by the long-term benefits of having a robust, reusable escrow infrastructure that can serve the entire program ecosystem.