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.
Recommended Fix
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.