Skip to main content
This guide outlines critical security considerations and best practices when developing custom adaptors for the Voltr Protocol.

Account Security

1. Authority Validation

// Always validate authorities and signers
require!(
    ctx.accounts.vault_strategy_auth.key() == expected_auth,
    AdaptorError::InvalidAccountOwner
);

// Verify protocol program ownership
require!(
    ctx.accounts.protocol_program.key() == strategy.protocol_program,
    AdaptorError::InvalidProtocolProgram
);
Key areas to validate:
  • Strategy authority signatures
  • Protocol program ownership
  • Token account authorities
  • Account derivation paths
  • PDA validation

2. Token Account Safety

fn validate_token_accounts(
    reserve_info: &AccountInfo,
    vault_asset_mint: Pubkey,
    reserve_collateral_mint_account: Pubkey,
    user_destination_collateral: Pubkey,
    vault_strategy_auth: Pubkey,
    collateral_token_program: Pubkey
) -> Result<()> {
    require!(
        reserve_liquidity_mint == vault_asset_mint.key(),
        AdaptorError::InvalidReserveAccount
    );

    require!(
        get_associated_token_address_with_program_id(
            &vault_strategy_auth.key(),
            &reserve_collateral_mint_account.key(),
            &collateral_token_program.key()
        ) == user_destination_collateral.key(),
        AdaptorError::InvalidAccountInput
    );

    Ok(())
}

3. PDA Derivation Security

[strategy] = PublicKey.findProgramAddressSync(
    [SEEDS.STRATEGY, counterparty_asset_ta.key().as_ref()],
    DEFAULT_ADAPTOR_PROGRAM_ID
);

#[account(
    seeds = [constants::STRATEGY_SEED, strategy.load()?.counterparty_asset_ta.key().as_ref()],
    bump = strategy.load()?.bump
)]
pub strategy: AccountLoader<'info, Strategy>,
Key considerations: Use consistent seed ordering, store and validate bumps, check PDA ownership, verify seed values, validate authority PDAs.

State Management Security

Position Value Tracking

fn calculate_current_amount(
    collateral_amount: u64,
    reserve_info: &AccountInfo
) -> Result<u64> {
    let total_liquidity = available_amount
        .checked_add(borrowed_amount)
        .ok_or(AdaptorError::MathOverflow)?;

    let liquidity_amount = (collateral_amount as u128)
        .checked_mul(total_liquidity)
        .ok_or(AdaptorError::MathOverflow)?
        .checked_div(total_supply)
        .ok_or(AdaptorError::MathOverflow)? as u64;

    Ok(liquidity_amount)
}

State Updates

impl Strategy {
    pub fn update_position(&mut self, value: u64) -> Result<()> {
        require!(value >= 0, AdaptorError::InvalidAmount);
        self.position_value = value;
        self.last_updated_ts = Clock::get()?.unix_timestamp;
        Ok(())
    }
}

Protocol Integration Security

CPI Safety

fn perform_protocol_operation(
    ctx: Context,
    args: ProtocolArgs
) -> Result<()> {
    validate_protocol_accounts(ctx.remaining_accounts)?;

    let cpi_ctx = CpiContext::new_with_signer(
        ctx.accounts.protocol_program.to_account_info(),
        protocol_accounts,
        &[&authority_seeds]
    );

    protocol::cpi::operation(cpi_ctx, args)?;

    Ok(())
}

Protocol State Validation

fn validate_protocol_state(
    protocol_account: &AccountInfo,
    expected_state: ProtocolState
) -> Result<()> {
    let state = ProtocolState::try_from_slice(&protocol_account.data.borrow())?;

    require!(
        state.is_valid_for_operation(),
        AdaptorError::InvalidProtocolState
    );

    validate_protocol_constraints(&state)?;

    Ok(())
}

Error Handling

Comprehensive Error Types

#[error_code]
pub enum AdaptorError {
    #[msg("Invalid amount provided.")]
    InvalidAmount,
    #[msg("Invalid account owner.")]
    InvalidAccountOwner,
    #[msg("Invalid token mint.")]
    InvalidTokenMint,
    #[msg("Math overflow.")]
    MathOverflow,
    #[msg("Invalid protocol state.")]
    InvalidProtocolState,
    #[msg("Protocol constraint violated.")]
    ProtocolConstraintViolation,
}

Input Validation

fn validate_operation_inputs(
    amount: u64,
    args: &OperationArgs
) -> Result<()> {
    require!(amount > 0, AdaptorError::InvalidAmount);
    require!(amount <= max_amount, AdaptorError::InvalidAmount);

    if let Some(args) = args {
        validate_args(args)?;
    }

    Ok(())
}

Operational Security

Transaction Atomicity

pub fn handle_protocol_operation(ctx: Context) -> Result<()> {
    let pre_state = validate_pre_state(ctx)?;
    protocol_operation(ctx)?;
    validate_post_state(ctx, pre_state)?;
    Ok(())
}

Upgrade Safety

#[account(zero_copy(unsafe))]
pub struct Strategy {
    pub version: u8,
    pub _reserved: [u8; 64],
}

Testing Requirements

  1. Security Tests: Authority validation, account validation, state consistency, error handling, edge cases
  2. Integration Tests: Protocol interactions, state transitions, error conditions, upgrade paths, multi-instruction scenarios
  3. Fuzzing Tests: Input validation, state mutations, account combinations, error conditions, protocol interactions

Security Checklist

  • All account owners validated
  • PDA derivation verified
  • Token accounts validated
  • Authority checks implemented
  • Account constraints enforced
  • Atomic updates implemented
  • Position tracking accurate
  • State consistency maintained
  • Version handling added
  • Upgrade path defined
  • CPI safety implemented
  • Protocol state validated
  • Error handling complete
  • Return values checked
  • Protocol constraints enforced
  • Security tests complete
  • Integration tests passed
  • Fuzzing performed
  • Edge cases covered
  • Upgrade tested