Skip to content

Latest commit

 

History

History

README.md

Type Casting Issues - Security Example

Vulnerability Overview

Severity: HIGH

This example demonstrates Type Casting Vulnerabilities, where using Rust's as keyword for type conversions can silently truncate values, leading to incorrect calculations.

The Problem

Rust's as keyword performs unchecked type conversions. When casting from a larger type to a smaller one (e.g., u64 to u32), the high bits are silently discarded:

let big: u64 = 5_000_000_000;  // 5 billion
let small = big as u32;        // 705,032,704 (TRUNCATED!)

In financial calculations, this can cause massive losses or enable exploits.

Real-World Impact

This vulnerability can lead to:

  • Reward calculation errors - Users receive far less than entitled
  • Fee bypass - Large values truncate to small fees
  • Integer manipulation - Attackers craft values that truncate favorably
  • Accounting errors - Balance tracking becomes incorrect

What You'll Learn

  1. How as casting silently truncates values
  2. The difference between as, try_from, and try_into
  3. Using u128 for intermediate calculations
  4. Safe patterns for type conversions in financial code

Program Structure

This program implements a reward distribution system:

  1. initialize_pool - Creates a reward pool
  2. initialize_user - Creates a user with shares
  3. calculate_reward_vulnerable - Uses unsafe as casting (VULNERABLE)
  4. calculate_reward_secure - Uses checked conversions (SECURE)

The Vulnerability: Unsafe Type Casting

Vulnerable Code

Located in: src/instructions/calculate_reward_vulnerable.rs

pub fn handler(ctx: Context<CalculateRewardVulnerable>) -> Result<u64> {
    let user = &ctx.accounts.user_account;
    let pool = &ctx.accounts.pool;

    // ⚠️ VULNERABLE: Casting u64 to u32 truncates large values!
    let user_shares_truncated = user.shares as u32;
    let reward_rate_truncated = pool.reward_rate as u32;

    // Calculate with truncated values - WRONG!
    let reward = user_shares_truncated as u64 * reward_rate_truncated as u64;

    let total_shares_truncated = pool.total_shares as u32;
    let final_reward = reward / total_shares_truncated as u64;

    Ok(final_reward)
}

Why This Is Dangerous

The as keyword in Rust:

From To Behavior
u64 u32 Truncates high 32 bits
u64 u16 Truncates high 48 bits
i64 u64 Reinterprets sign bit
u64 i32 Truncates AND reinterprets

Truncation Example

Original u64 value: 5,000,000,000 (0x12A05F200)

Binary: 0000 0000 0000 0000 0000 0001 0010 1010
        0000 0101 1111 0010 0000 0000 0000 0000

After `as u32` (keep only low 32 bits):
        0010 1010 0000 0101 1111 0010 0000 0000

Result: 705,032,704

Lost: 4,294,967,296 (86% of the value!)

The Attack

Scenario: Reward pool with large values

Pool Configuration:
- total_shares: 10,000,000,000 (10 billion)
- reward_rate: 1,000,000

User has:
- shares: 5,000,000,000 (5 billion, 50% of pool)

Expected reward: 50% of rewards = 500,000

VULNERABLE CALCULATION:
1. user.shares as u32 = 705,032,704 (truncated!)
2. reward_rate as u32 = 1,000,000 (fits, no truncation)
3. total_shares as u32 = 1,410,065,408 (truncated!)
4. reward = 705,032,704 * 1,000,000 / 1,410,065,408
5. reward ≈ 499,999 (accidentally close, but wrong math!)

With different values, errors can be MASSIVE!

Attack Code Example

// Attacker creates account with carefully chosen shares
// that truncate to a favorable value
const maliciousShares = new BN("4294967296"); // 2^32, truncates to 0!

await program.methods
  .initializeUser(maliciousShares)
  .accounts({...})
  .rpc();

// Or: shares that truncate to same value as larger holder
// shares = 0x100000000 + small_amount truncates to small_amount

The Fix: Safe Type Conversions

Located in: src/instructions/calculate_reward_secure.rs

pub fn handler(ctx: Context<CalculateRewardSecure>) -> Result<u64> {
    let user = &ctx.accounts.user_account;
    let pool = &ctx.accounts.pool;

    // ✅ SECURE: Use u128 for intermediate calculation
    let numerator = (user.shares as u128)
        .checked_mul(pool.reward_rate as u128)
        .ok_or(ErrorCode::Overflow)?;

    let reward_u128 = numerator
        .checked_div(pool.total_shares as u128)
        .ok_or(ErrorCode::Overflow)?;

    // ✅ SECURE: Verify result fits before converting
    let final_reward = u64::try_from(reward_u128)
        .map_err(|_| ErrorCode::Overflow)?;

    Ok(final_reward)
}

Safe Conversion Methods

Method Behavior Use Case
as Silent truncation AVOID for values
try_from() Returns Result Converting down
try_into() Returns Result Converting down
from() Infallible Converting up (u32→u64)

Safe Patterns

Pattern 1: Use Larger Types for Intermediate Values

// Multiplying two u64 values could overflow
// Use u128 for the intermediate result
let result = (a as u128)
    .checked_mul(b as u128)?
    .checked_div(c as u128)?;

let final_result = u64::try_from(result)?;

Pattern 2: Validate Before Casting

// If you must cast down, validate first
require!(value <= u32::MAX as u64, ErrorCode::ValueTooLarge);
let small_value = value as u32;

Pattern 3: Use try_from/try_into

// Returns Err if value doesn't fit
let small: u32 = u32::try_from(large_value)
    .map_err(|_| ErrorCode::TruncationError)?;

Key Takeaways

DON'T

  • Use as for downcasting (u64 → u32, u64 → u16)
  • Assume values will always fit in smaller types
  • Cast user-provided values without validation
  • Use smaller types to "save space" in calculations

DO

  • Use try_from() / try_into() for safe conversions
  • Use u128 for intermediate multiplication results
  • Validate values before necessary downcasts
  • Keep financial values as u64 throughout calculations

Common Truncation Boundaries

Type Max Value Watch Out For
u8 255 Anything > 255
u16 65,535 Anything > 65K
u32 4,294,967,295 Anything > 4.3B
u64 18.4 quintillion Multiplication overflow

Building This Example

cd 7-type-casting
anchor build

Additional Resources


Remember: Never use as for downcasting in financial calculations. The silent truncation can turn a 5 billion balance into 700 million without any error!