security research12 min read

Top Rust Smart Contract Vulnerabilities in 2025: Real-World Examples and Fixes

BlockSecOps Team
Top Rust Smart Contract Vulnerabilities in 2025: Real-World Examples and Fixes

As Rust-based blockchain platforms like Solana continue to gain traction in 2025, understanding common security vulnerabilities has become critical for developers building decentralized applications. While Rust's memory safety guarantees eliminate entire classes of bugs, smart contract development introduces unique challenges that can lead to severe exploits.

In this comprehensive guide, we'll explore the most prevalent Rust smart contract vulnerabilities discovered in 2024-2025, complete with vulnerable code examples and their fixes.

1. Integer Overflow and Underflow

The Vulnerability

Integer overflow is one of the most common vulnerabilities in Rust smart contracts. While Rust checks for integer overflow in debug mode, it does not check in release mode—which is what Solana uses by default. This can lead to catastrophic failures in token calculations, balance updates, and financial operations.

Vulnerable Code Example

use anchor_lang::prelude::*;

#[program]
pub mod vulnerable_token {
    use super::*;

    pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
        let from = &mut ctx.accounts.from;
        let to = &mut ctx.accounts.to;

        // VULNERABLE: No overflow checking
        from.balance = from.balance - amount;  // Can underflow
        to.balance = to.balance + amount;      // Can overflow

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Transfer<'info> {
    #[account(mut)]
    pub from: Account<'info, TokenAccount>,
    #[account(mut)]
    pub to: Account<'info, TokenAccount>,
}

#[account]
pub struct TokenAccount {
    pub balance: u64,
    pub owner: Pubkey,
}

Attack Scenario: An attacker transfers more tokens than they have, causing an underflow that wraps the balance to a massive number (u64::MAX).

Fixed Code

use anchor_lang::prelude::*;

#[program]
pub mod secure_token {
    use super::*;

    pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
        let from = &mut ctx.accounts.from;
        let to = &mut ctx.accounts.to;

        // SECURE: Use checked arithmetic
        from.balance = from.balance
            .checked_sub(amount)
            .ok_or(ErrorCode::InsufficientFunds)?;

        to.balance = to.balance
            .checked_add(amount)
            .ok_or(ErrorCode::Overflow)?;

        Ok(())
    }
}

#[error_code]
pub enum ErrorCode {
    #[msg("Insufficient funds for transfer")]
    InsufficientFunds,
    #[msg("Balance overflow")]
    Overflow,
}

Key Fix: Always use checked_add(), checked_sub(), checked_mul(), and checked_div() for financial calculations.


2. Missing Signer Authorization

The Vulnerability

One of the most critical vulnerabilities in Solana programs is failing to verify that an account has actually signed a transaction. Without proper signer checks, attackers can execute privileged operations on behalf of other users.

Vulnerable Code Example

use anchor_lang::prelude::*;

#[program]
pub mod vulnerable_vault {
    use super::*;

    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        let vault = &mut ctx.accounts.vault;
        let user = &ctx.accounts.user;

        // VULNERABLE: No signer check!
        // Anyone can call this and withdraw from any user's vault

        vault.balance = vault.balance
            .checked_sub(amount)
            .ok_or(ErrorCode::InsufficientFunds)?;

        // Transfer logic here...

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut)]
    pub vault: Account<'info, Vault>,
    /// CHECK: No validation - DANGEROUS!
    pub user: AccountInfo<'info>,
}

#[account]
pub struct Vault {
    pub owner: Pubkey,
    pub balance: u64,
}

Attack Scenario: An attacker calls withdraw() with someone else's vault account and drains their funds.

Fixed Code

use anchor_lang::prelude::*;

#[program]
pub mod secure_vault {
    use super::*;

    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        let vault = &mut ctx.accounts.vault;

        // SECURE: Anchor automatically validates the signer
        // constraint through the #[account] attribute

        vault.balance = vault.balance
            .checked_sub(amount)
            .ok_or(ErrorCode::InsufficientFunds)?;

        // Transfer logic here...

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(
        mut,
        has_one = owner @ ErrorCode::Unauthorized
    )]
    pub vault: Account<'info, Vault>,

    #[account(mut)]
    pub owner: Signer<'info>, // Enforces signer check
}

#[account]
pub struct Vault {
    pub owner: Pubkey,
    pub balance: u64,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Insufficient funds")]
    InsufficientFunds,
    #[msg("Unauthorized access")]
    Unauthorized,
}

Key Fix: Use Signer<'info> type and has_one constraint to enforce authorization.


3. Account Ownership Validation

The Vulnerability

Solana programs must validate that accounts are owned by the correct program. Failing to do so allows attackers to pass malicious accounts that could manipulate program state.

Vulnerable Code Example

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program_pack::Pack;
use spl_token::state::Account as SplTokenAccount;

#[program]
pub mod vulnerable_swap {
    use super::*;

    pub fn swap(ctx: Context<Swap>, amount: u64) -> Result<()> {
        // VULNERABLE: No ownership validation on token_account
        let token_account_info = &ctx.accounts.token_account;

        // Deserialize without checking owner
        let token_account = SplTokenAccount::unpack(
            &token_account_info.try_borrow_data()?
        )?;

        // Use token_account data...
        msg!("Token balance: {}", token_account.amount);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Swap<'info> {
    /// CHECK: Not validated - DANGEROUS!
    pub token_account: AccountInfo<'info>,
}

Attack Scenario: An attacker creates a fake account with manipulated data that passes as a token account.

Fixed Code

use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};

#[program]
pub mod secure_swap {
    use super::*;

    pub fn swap(ctx: Context<Swap>, amount: u64) -> Result<()> {
        // SECURE: Anchor validates ownership automatically
        let token_account = &ctx.accounts.token_account;

        msg!("Token balance: {}", token_account.amount);

        // Additional validation
        require!(
            token_account.owner == ctx.accounts.user.key(),
            ErrorCode::InvalidTokenAccount
        );

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Swap<'info> {
    #[account(mut)]
    pub user: Signer<'info>,

    // SECURE: Anchor validates this is a real SPL token account
    #[account(
        mut,
        constraint = token_account.owner == user.key() @ ErrorCode::InvalidTokenAccount
    )]
    pub token_account: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Invalid token account")]
    InvalidTokenAccount,
}

Key Fix: Use Anchor's Account<'info, T> type which automatically validates account ownership and deserializes data safely.


4. Precision Loss in Financial Calculations

The Vulnerability

Using floating-point arithmetic or incorrect integer division can lead to precision loss, which attackers can exploit through dust attacks or repeated rounding errors.

Vulnerable Code Example

use anchor_lang::prelude::*;

#[program]
pub mod vulnerable_staking {
    use super::*;

    pub fn calculate_rewards(ctx: Context<Rewards>, days: u64) -> Result<()> {
        let stake = &ctx.accounts.stake;

        // VULNERABLE: Integer division loses precision
        let daily_rate = 100; // 1% = 100 basis points
        let reward = (stake.amount * daily_rate * days) / 10000;

        // For small amounts, this can round down to 0
        // Attacker can exploit by making many small stakes

        msg!("Reward: {}", reward);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Rewards<'info> {
    pub stake: Account<'info, Stake>,
}

#[account]
pub struct Stake {
    pub amount: u64,
    pub owner: Pubkey,
}

Attack Scenario: User stakes small amounts repeatedly to exploit rounding errors in their favor.

Fixed Code

use anchor_lang::prelude::*;

const PRECISION: u128 = 1_000_000; // 6 decimal places
const DAILY_RATE: u128 = 10_000; // 1% with precision (1% = 0.01 * 1_000_000)

#[program]
pub mod secure_staking {
    use super::*;

    pub fn calculate_rewards(ctx: Context<Rewards>, days: u64) -> Result<()> {
        let stake = &ctx.accounts.stake;

        // SECURE: Use high-precision arithmetic
        let amount_precise = (stake.amount as u128)
            .checked_mul(PRECISION)
            .ok_or(ErrorCode::MathOverflow)?;

        let reward_precise = amount_precise
            .checked_mul(DAILY_RATE)
            .ok_or(ErrorCode::MathOverflow)?
            .checked_mul(days as u128)
            .ok_or(ErrorCode::MathOverflow)?
            .checked_div(100 * PRECISION) // 100 = 100%
            .ok_or(ErrorCode::MathOverflow)?;

        let reward = reward_precise
            .checked_div(PRECISION)
            .ok_or(ErrorCode::MathOverflow)?;

        require!(
            reward <= u64::MAX as u128,
            ErrorCode::RewardTooLarge
        );

        msg!("Reward: {}", reward);
        Ok(())
    }
}

#[error_code]
pub enum ErrorCode {
    #[msg("Math overflow")]
    MathOverflow,
    #[msg("Reward too large")]
    RewardTooLarge,
}

Key Fix: Use fixed-point arithmetic with sufficient precision (typically 6-9 decimal places) and u128 for intermediate calculations.


5. Improper PDA Seed Validation

The Vulnerability

Program Derived Addresses (PDAs) are a unique Solana feature. If PDA seeds aren't properly validated, attackers can create colliding addresses or access unauthorized PDAs.

Vulnerable Code Example

use anchor_lang::prelude::*;

#[program]
pub mod vulnerable_escrow {
    use super::*;

    pub fn create_escrow(
        ctx: Context<CreateEscrow>,
        escrow_id: u64,
        amount: u64,
    ) -> Result<()> {
        let escrow = &mut ctx.accounts.escrow;

        // VULNERABLE: No validation that PDA was derived correctly
        escrow.id = escrow_id;
        escrow.amount = amount;
        escrow.sender = ctx.accounts.sender.key();

        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(escrow_id: u64)]
pub struct CreateEscrow<'info> {
    #[account(
        init,
        payer = sender,
        space = 8 + 32 + 8 + 8
    )]
    pub escrow: Account<'info, Escrow>,

    #[account(mut)]
    pub sender: Signer<'info>,

    pub system_program: Program<'info, System>,
}

Attack Scenario: Attacker provides a non-PDA account they control instead of the expected PDA.

Fixed Code

use anchor_lang::prelude::*;

#[program]
pub mod secure_escrow {
    use super::*;

    pub fn create_escrow(
        ctx: Context<CreateEscrow>,
        escrow_id: u64,
        amount: u64,
    ) -> Result<()> {
        let escrow = &mut ctx.accounts.escrow;

        // SECURE: Anchor validates PDA through seeds constraint
        escrow.id = escrow_id;
        escrow.amount = amount;
        escrow.sender = ctx.accounts.sender.key();
        escrow.bump = ctx.bumps.escrow;

        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(escrow_id: u64)]
pub struct CreateEscrow<'info> {
    #[account(
        init,
        payer = sender,
        space = 8 + 32 + 8 + 8 + 1,
        seeds = [
            b"escrow",
            sender.key().as_ref(),
            escrow_id.to_le_bytes().as_ref()
        ],
        bump
    )]
    pub escrow: Account<'info, Escrow>,

    #[account(mut)]
    pub sender: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[account]
pub struct Escrow {
    pub id: u64,
    pub amount: u64,
    pub sender: Pubkey,
    pub bump: u8,
}

Key Fix: Always use the seeds and bump constraints to ensure PDAs are derived correctly.


6. Reinitialization Attacks

The Vulnerability

Failing to track initialization state allows attackers to reinitialize accounts, potentially overwriting critical data or bypassing security checks.

Vulnerable Code Example

use anchor_lang::prelude::*;

#[program]
pub mod vulnerable_init {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, admin: Pubkey) -> Result<()> {
        let config = &mut ctx.accounts.config;

        // VULNERABLE: No check if already initialized
        config.admin = admin;
        config.total_supply = 1_000_000;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub config: Account<'info, Config>,
}

#[account]
pub struct Config {
    pub admin: Pubkey,
    pub total_supply: u64,
}

Attack Scenario: Attacker calls initialize() again to change the admin address after deployment.

Fixed Code

use anchor_lang::prelude::*;

#[program]
pub mod secure_init {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, admin: Pubkey) -> Result<()> {
        let config = &mut ctx.accounts.config;

        // SECURE: Anchor's 'init' constraint prevents reinitialization
        config.admin = admin;
        config.total_supply = 1_000_000;
        config.is_initialized = true;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = payer,
        space = 8 + 32 + 8 + 1
    )]
    pub config: Account<'info, Config>,

    #[account(mut)]
    pub payer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[account]
pub struct Config {
    pub admin: Pubkey,
    pub total_supply: u64,
    pub is_initialized: bool,
}

Key Fix: Use Anchor's init constraint which automatically prevents reinitialization. For additional safety, add an is_initialized flag.


7. Unvalidated Cross-Program Invocation (CPI)

The Vulnerability

When making cross-program invocations, programs must validate the invoked program ID. Failing to do so allows attackers to substitute malicious programs.

Vulnerable Code Example

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::invoke;

#[program]
pub mod vulnerable_cpi {
    use super::*;

    pub fn proxy_transfer(
        ctx: Context<ProxyTransfer>,
        amount: u64,
    ) -> Result<()> {
        // VULNERABLE: Not validating the program being called
        let ix = spl_token::instruction::transfer(
            ctx.accounts.token_program.key,
            ctx.accounts.from.key,
            ctx.accounts.to.key,
            ctx.accounts.authority.key,
            &[],
            amount,
        )?;

        invoke(
            &ix,
            &[
                ctx.accounts.from.to_account_info(),
                ctx.accounts.to.to_account_info(),
                ctx.accounts.authority.to_account_info(),
            ],
        )?;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct ProxyTransfer<'info> {
    /// CHECK: Not validated - DANGEROUS!
    pub token_program: AccountInfo<'info>,
    /// CHECK: Not validated
    pub from: AccountInfo<'info>,
    /// CHECK: Not validated
    pub to: AccountInfo<'info>,
    pub authority: Signer<'info>,
}

Attack Scenario: Attacker provides a malicious program instead of the real SPL Token program.

Fixed Code

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};

#[program]
pub mod secure_cpi {
    use super::*;

    pub fn proxy_transfer(
        ctx: Context<ProxyTransfer>,
        amount: u64,
    ) -> Result<()> {
        // SECURE: Using Anchor's CPI module with validated accounts
        let cpi_accounts = Transfer {
            from: ctx.accounts.from.to_account_info(),
            to: ctx.accounts.to.to_account_info(),
            authority: ctx.accounts.authority.to_account_info(),
        };

        let cpi_program = ctx.accounts.token_program.to_account_info();
        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);

        token::transfer(cpi_ctx, amount)?;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct ProxyTransfer<'info> {
    #[account(mut)]
    pub from: Account<'info, TokenAccount>,

    #[account(mut)]
    pub to: Account<'info, TokenAccount>,

    pub authority: Signer<'info>,

    // SECURE: Anchor validates this is the real Token program
    pub token_program: Program<'info, Token>,
}

Key Fix: Use Anchor's Program<'info, T> type and CPI helpers which automatically validate program IDs.


Real-World Impact: The Loopscale Exploit (April 2025)

In April 2025, the Loopscale protocol suffered a $5.8 million exploit due to a critical logical vulnerability in their token value calculation. While not a traditional smart contract bug, this incident demonstrates that even with Rust's memory safety, logical errors in financial calculations can have catastrophic consequences.

The exploit highlighted the importance of:

  • Comprehensive testing of edge cases in financial logic
  • Formal verification for critical calculation functions
  • Time-locks and upgradability for emergency responses
  • External audits before mainnet deployment

Security Best Practices for 2025

1. Use Anchor Framework

Anchor provides built-in security features and reduces boilerplate, making it harder to introduce vulnerabilities.

2. Enable Overflow Checks

Add to your Cargo.toml:

[profile.release]
overflow-checks = true

3. Comprehensive Testing

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_overflow_protection() {
        // Test with u64::MAX
        let result = u64::MAX.checked_add(1);
        assert!(result.is_none());
    }

    #[test]
    fn test_authorization() {
        // Test unauthorized access attempts
    }

    #[test]
    fn test_precision_loss() {
        // Test small amount calculations
    }
}

4. Use Static Analysis Tools

  • Soteria: Solana-specific vulnerability scanner
  • Clippy: Rust linter with security checks
  • Anchor Verify: Verifies deployed programs match source code

5. Professional Audits

Always get a professional security audit before mainnet deployment. Notable audit firms include:

  • Halborn
  • OtterSec
  • Neodyme
  • Trail of Bits
  • Kudelski Security

Conclusion

Rust's memory safety is a significant advantage for smart contract development, but it doesn't eliminate all security risks. The vulnerabilities discussed in this article represent real threats observed in 2024-2025 production systems.

Key Takeaways:

  1. Always use checked arithmetic for financial operations
  2. Validate all account inputs rigorously
  3. Leverage Anchor's safety features whenever possible
  4. Test edge cases extensively, especially overflow conditions
  5. Get professional audits before deploying to mainnet

By following these practices and staying informed about emerging threats, you can build more secure Rust smart contracts and protect your users' assets.


Disclaimer: The vulnerable code examples in this article are for educational purposes only. Never deploy similar patterns to production systems.

Secure Your Web3 Project with BlockSecOps

BlockSecOps is a comprehensive DevSecOps platform built specifically for Web3 development. We help you integrate security throughout your development lifecycle—from smart contract auditing and vulnerability scanning to automated testing and continuous monitoring. Build with confidence knowing your blockchain applications are protected at every stage.

Learn more about BlockSecOps