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.
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.
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
- How
ascasting silently truncates values - The difference between
as,try_from, andtry_into - Using u128 for intermediate calculations
- Safe patterns for type conversions in financial code
This program implements a reward distribution system:
initialize_pool- Creates a reward poolinitialize_user- Creates a user with sharescalculate_reward_vulnerable- Uses unsafeascasting (VULNERABLE)calculate_reward_secure- Uses checked conversions (SECURE)
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)
}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 |
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!)
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!
// 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_amountLocated 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)
}| 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) |
// 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)?;// If you must cast down, validate first
require!(value <= u32::MAX as u64, ErrorCode::ValueTooLarge);
let small_value = value as u32;// Returns Err if value doesn't fit
let small: u32 = u32::try_from(large_value)
.map_err(|_| ErrorCode::TruncationError)?;- Use
asfor 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
- 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
| 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 |
cd 7-type-casting
anchor buildRemember: Never use as for downcasting in financial calculations. The silent truncation can turn a 5 billion balance into 700 million without any error!