Skip to main content
Sigvex

Critical Truncation Remediation

How to fix integer truncation in lamport transfers, account storage, and access control.

Critical Truncation Remediation

Overview

Related Detector: Critical Truncation

Integer truncation flowing into lamport transfers, account storage, or access control branches creates immediate security risks including fund theft, state corruption, and authorization bypass. The fix is to use full-width types for all sensitive operations and add explicit bounds validation when downcasts are unavoidable.

Before (Vulnerable)

pub fn process_transfer(
    accounts: &[AccountInfo],
    amount: u64,
) -> ProgramResult {
    // VULNERABLE: truncation in transfer pipeline
    let transfer_amount: u32 = amount as u32;
    **accounts[0].try_borrow_mut_lamports()? -= transfer_amount as u64;
    **accounts[1].try_borrow_mut_lamports()? += transfer_amount as u64;
    Ok(())
}

After (Fixed)

pub fn process_transfer(
    accounts: &[AccountInfo],
    amount: u64,
) -> ProgramResult {
    // FIXED: use u64 throughout the transfer pipeline
    let balance = **accounts[0].try_borrow_lamports()?;
    if balance < amount {
        return Err(ProgramError::InsufficientFunds);
    }

    **accounts[0].try_borrow_mut_lamports()? -= amount;
    **accounts[1].try_borrow_mut_lamports()? += amount;
    Ok(())
}

Alternative Mitigations

1. Validate before storing to account data

pub fn store_value(account: &AccountInfo, value: u64) -> ProgramResult {
    // If u16 storage is required, validate range first
    let stored: u16 = value
        .try_into()
        .map_err(|_| ProgramError::ArithmeticOverflow)?;

    let data = &mut account.try_borrow_mut_data()?;
    data[0..2].copy_from_slice(&stored.to_le_bytes());
    Ok(())
}

2. Use full-width storage

pub fn store_balance(account: &AccountInfo, balance: u64) -> ProgramResult {
    // Use 8 bytes for u64 -- no truncation needed
    let data = &mut account.try_borrow_mut_data()?;
    data[0..8].copy_from_slice(&balance.to_le_bytes());
    Ok(())
}

3. Enum-based access control

Replace numeric role IDs with typed enums to eliminate truncation risk:

#[derive(Clone, Copy, PartialEq)]
pub enum Role {
    User = 0,
    Moderator = 1,
    Admin = 2,
}

impl TryFrom<u8> for Role {
    type Error = ProgramError;
    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Role::User),
            1 => Ok(Role::Moderator),
            2 => Ok(Role::Admin),
            _ => Err(ProgramError::InvalidArgument),
        }
    }
}

Common Mistakes

Mistake 1: Casting back after truncation

// WRONG: data already lost at the truncation point
let small: u32 = big_value as u32;
let restored: u64 = small as u64;  // Not the original value!

Once bits are discarded by truncation, they cannot be recovered by widening.

Mistake 2: Truncating before comparison

// WRONG: comparison on truncated values is meaningless
if (amount as u32) <= (max_allowed as u32) {
    transfer(amount)?;  // Original u64 amount, not the truncated one
}

Compare values in their original types. Never truncate just for comparison.

Mistake 3: Assuming small account data means small values

// WRONG: account data could contain serialized u64
let data = account.try_borrow_data()?;
let value: u16 = u16::from_le_bytes(data[0..2].try_into().unwrap());
// If the stored value was actually u64, only low 2 bytes are read

Read the correct number of bytes for the stored type and validate the full value.

References