3. Numeric / Bitwise Pitfalls
Overview
Move’s numeric and bitwise operations have specific semantics that differ from other languages. Arithmetic operations abort on overflow/underflow (rather than wrapping), while bitwise shifts beyond the type width silently produce zero. These behaviors can lead to unexpected results and security vulnerabilities.
Risk Level
Medium to High — Can cause denial of service or bypass access control checks.
OWASP / CWE Mapping
| OWASP Top 10 | MITRE CWE |
|---|---|
| A06 (Vulnerable Components), A03 (Injection) | CWE-681 (Incorrect Conversion), CWE-190 (Integer Overflow) |
The Problem
Overflow/Underflow Behavior
Move aborts on overflow rather than wrapping:
let x: u64 = 18446744073709551615; // MAX_U64
let y = x + 1; // ABORTS! Not wrapping to 0This is generally safer than wrapping, but can be exploited for DoS attacks.
Bitwise Shift Behavior
Shifts beyond type width silently produce zero:
let x: u64 = 1;
let y = x << 64; // Returns 0, not an error!
let z = x << 100; // Also returns 0This can bypass role/permission checks that use bitmasks.
Vulnerable Example
module vulnerable::roles {
use sui::object::{Self, UID};
use sui::tx_context::TxContext;
const E_NOT_AUTHORIZED: u64 = 1;
public struct RoleManager has key {
id: UID,
/// Bitmask of roles: bit 0 = admin, bit 1 = operator, etc.
user_roles: vector<u64>, // indexed by user_id
}
/// VULNERABLE: No validation of role_index
public fun has_role(
manager: &RoleManager,
user_id: u64,
role_index: u64
): bool {
let roles = *vector::borrow(&manager.user_roles, user_id);
// VULNERABLE: If role_index >= 64, this returns 0!
// Attacker can bypass role check by passing role_index = 64
let bit = 1u64 << role_index;
(roles & bit) != 0
}
/// VULNERABLE: Attacker can set arbitrary roles
public entry fun grant_role(
manager: &mut RoleManager,
user_id: u64,
role_index: u64,
ctx: &mut TxContext
) {
// If role_index >= 64, bit = 0, and roles unchanged
// But function appears to succeed!
let bit = 1u64 << role_index;
let roles = vector::borrow_mut(&mut manager.user_roles, user_id);
*roles = *roles | bit;
}
}
module vulnerable::fees {
const MAX_FEE_BPS: u64 = 10000; // 100%
public struct FeeConfig has key {
id: UID,
fee_bps: u64,
}
/// VULNERABLE: Can cause DoS via overflow abort
public fun calculate_fee(config: &FeeConfig, amount: u64): u64 {
// If amount * fee_bps overflows, transaction aborts
// Attacker can prevent any transaction with large amounts
(amount * config.fee_bps) / MAX_FEE_BPS
}
/// VULNERABLE: Underflow abort as DoS vector
public fun withdraw(balance: &mut u64, amount: u64) {
// Attacker can cause abort by requesting more than balance
*balance = *balance - amount; // Aborts if amount > balance
}
}Attack Scenarios
Shift-based bypass:
// Attacker calls:
let can_admin = has_role(manager, my_id, 64);
// Returns false (bit = 0), but the check is meaningless
// Attacker found a way to make role_index bypass validationOverflow DoS:
// Attacker creates scenario where amount * fee_bps overflows
// All fee calculations abort, protocol becomes unusable
let fee = calculate_fee(config, 18446744073709551615); // Aborts!Secure Example
module secure::roles {
use sui::object::{Self, UID};
use sui::tx_context::TxContext;
const E_INVALID_ROLE_INDEX: u64 = 1;
const E_NOT_AUTHORIZED: u64 = 2;
const MAX_ROLE_INDEX: u64 = 63; // Maximum valid bit position for u64
public struct RoleManager has key {
id: UID,
user_roles: vector<u64>,
}
/// SECURE: Validate shift amount before use
public fun has_role(
manager: &RoleManager,
user_id: u64,
role_index: u64
): bool {
// Validate role_index is within valid range
assert!(role_index <= MAX_ROLE_INDEX, E_INVALID_ROLE_INDEX);
let roles = *vector::borrow(&manager.user_roles, user_id);
let bit = 1u64 << role_index;
(roles & bit) != 0
}
/// SECURE: Use enumerated roles instead of arbitrary indices
public fun has_admin_role(manager: &RoleManager, user_id: u64): bool {
has_role(manager, user_id, 0) // Admin is always bit 0
}
public fun has_operator_role(manager: &RoleManager, user_id: u64): bool {
has_role(manager, user_id, 1) // Operator is always bit 1
}
}
module secure::fees {
const MAX_FEE_BPS: u64 = 10000;
const E_OVERFLOW: u64 = 1;
const E_INSUFFICIENT_BALANCE: u64 = 2;
public struct FeeConfig has key {
id: UID,
fee_bps: u64,
}
/// SECURE: Check for overflow before arithmetic
public fun calculate_fee(config: &FeeConfig, amount: u64): u64 {
// Use u128 for intermediate calculation to prevent overflow
let amount_128 = (amount as u128);
let fee_bps_128 = (config.fee_bps as u128);
let max_fee_bps_128 = (MAX_FEE_BPS as u128);
let result_128 = (amount_128 * fee_bps_128) / max_fee_bps_128;
// Safe to downcast since result <= amount
(result_128 as u64)
}
/// Alternative: Use checked arithmetic with explicit handling
public fun calculate_fee_checked(
config: &FeeConfig,
amount: u64
): Option<u64> {
// Check if multiplication would overflow
if (amount > 0 && config.fee_bps > 18446744073709551615 / amount) {
return option::none()
};
option::some((amount * config.fee_bps) / MAX_FEE_BPS)
}
/// SECURE: Explicit underflow check with meaningful error
public fun withdraw(balance: &mut u64, amount: u64) {
assert!(*balance >= amount, E_INSUFFICIENT_BALANCE);
*balance = *balance - amount;
}
}Recommended Mitigations
1. Validate Shift Amounts
const MAX_SHIFT: u8 = 63;
public fun safe_shift_left(value: u64, shift: u8): u64 {
assert!(shift <= MAX_SHIFT, E_INVALID_SHIFT);
value << (shift as u64)
}2. Use Wider Types for Intermediate Calculations
public fun safe_multiply_divide(a: u64, b: u64, divisor: u64): u64 {
let a_128 = (a as u128);
let b_128 = (b as u128);
let divisor_128 = (divisor as u128);
((a_128 * b_128) / divisor_128 as u64)
}3. Avoid Bitwise Logic for Access Control
/// Instead of bitmasks, use explicit role structs
public struct Roles has store, drop {
is_admin: bool,
is_operator: bool,
is_minter: bool,
}4. Use Checked Arithmetic Helpers
/// Returns None on overflow
public fun checked_add(a: u64, b: u64): Option<u64> {
if (a > 18446744073709551615 - b) {
option::none()
} else {
option::some(a + b)
}
}
/// Returns None on underflow
public fun checked_sub(a: u64, b: u64): Option<u64> {
if (a < b) {
option::none()
} else {
option::some(a - b)
}
}Testing Checklist
- Test all shift operations with edge values (0, 63, 64, 255)
- Test arithmetic operations with MAX values for overflow
- Test subtraction operations with edge cases for underflow
- Verify bitmask operations have proper bounds checking
- Test fee calculations with maximum possible amounts