Skip to main content
Sigvex

Integer Truncation Remediation

How to fix unsafe integer type casts that truncate data.

Integer Truncation Remediation

Overview

Related Detector: Integer Truncation

Unsafe type casts from larger to smaller integer types silently discard high bits, producing incorrect values. The fix is to use try_from/try_into conversions that return errors on overflow, add explicit bounds checks before casting, or redesign to use appropriate types throughout.

Before (Vulnerable)

pub fn process(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    // VULNERABLE: truncates values > u32::MAX to wrong amount
    let transfer_amount: u32 = amount as u32;
    execute_transfer(accounts, transfer_amount as u64)?;
    Ok(())
}

After (Fixed)

pub fn process(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    // FIXED: use try_into for safe conversion
    let transfer_amount: u32 = amount
        .try_into()
        .map_err(|_| ProgramError::ArithmeticOverflow)?;
    execute_transfer(accounts, transfer_amount as u64)?;
    Ok(())
}

Alternative Mitigations

1. Use full-width types throughout

The safest approach is to avoid downcasts entirely:

pub fn process(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    // No cast needed -- use u64 for the entire pipeline
    execute_transfer(accounts, amount)?;
    Ok(())
}

2. Explicit bounds check before cast

pub fn safe_cast_u64_to_u32(value: u64) -> Result<u32, ProgramError> {
    if value > u32::MAX as u64 {
        return Err(ProgramError::ArithmeticOverflow);
    }
    Ok(value as u32)
}

3. Saturating cast with documentation

When truncation is acceptable (e.g., for display or logging), use saturating casts and document the intent:

// Intentional: clamp to u16 range for display purposes only
let display_amount: u16 = amount.min(u16::MAX as u64) as u16;

Common Mistakes

Mistake 1: Casting both sides of a comparison

// WRONG: both sides truncated, comparison is meaningless
if (balance as u32) > (threshold as u32) {
    // Large values wrap, comparison fails
}

Compare values in their original type before any casting.

Mistake 2: Casting before arithmetic

// WRONG: truncation happens before the multiplication
let result = (large_value as u32) * rate;

Perform arithmetic at the widest type, then cast the result:

let result: u32 = (large_value * rate as u64)
    .try_into()
    .map_err(|_| ProgramError::ArithmeticOverflow)?;

Mistake 3: Assuming u64 values always fit in u32

// WRONG: lamport values can exceed u32::MAX (4.29 SOL)
let lamports: u32 = account.lamports() as u32;

Lamport balances are u64 and frequently exceed u32::MAX (4,294,967,295 lamports = ~4.29 SOL). Always use u64 for lamport values.

References