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.
letx: u64=18446744073709551615;// MAX_U64
lety=x+1;// ABORTS! Not wrapping to 0
This is generally safer than wrapping, but can be exploited for DoS attacks.
Bitwise Shift Behavior
Shifts beyond type width silently produce zero:
letx: u64=1;lety=x<<64;// Returns 0, not an error!
letz=x<<100;// Also returns 0
This can bypass role/permission checks that use bitmasks.
Vulnerable Example
modulevulnerable::roles{usesui::object::{Self,UID};usesui::tx_context::TxContext;constE_NOT_AUTHORIZED: u64=1;publicstructRoleManagerhaskey{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
publicfunhas_role(manager: &RoleManager,user_id: u64,role_index: u64): bool{letroles=*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
letbit=1u64<<role_index;(roles&bit)!=0}/// VULNERABLE: Attacker can set arbitrary roles
publicentryfungrant_role(manager: &mutRoleManager,user_id: u64,role_index: u64,ctx: &mutTxContext){// If role_index >= 64, bit = 0, and roles unchanged
// But function appears to succeed!
letbit=1u64<<role_index;letroles=vector::borrow_mut(&mutmanager.user_roles,user_id);*roles=*roles|bit;}}modulevulnerable::fees{constMAX_FEE_BPS: u64=10000;// 100%
publicstructFeeConfighaskey{id: UID,fee_bps: u64,}/// VULNERABLE: Can cause DoS via overflow abort
publicfuncalculate_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
publicfunwithdraw(balance: &mutu64,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:
letcan_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 validation
Overflow DoS:
// Attacker creates scenario where amount * fee_bps overflows
// All fee calculations abort, protocol becomes unusable
letfee=calculate_fee(config,18446744073709551615);// Aborts!
Secure Example
modulesecure::roles{usesui::object::{Self,UID};usesui::tx_context::TxContext;constE_INVALID_ROLE_INDEX: u64=1;constE_NOT_AUTHORIZED: u64=2;constMAX_ROLE_INDEX: u64=63;// Maximum valid bit position for u64
publicstructRoleManagerhaskey{id: UID,user_roles: vector<u64>,}/// SECURE: Validate shift amount before use
publicfunhas_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);letroles=*vector::borrow(&manager.user_roles,user_id);letbit=1u64<<role_index;(roles&bit)!=0}/// SECURE: Use enumerated roles instead of arbitrary indices
publicfunhas_admin_role(manager: &RoleManager,user_id: u64): bool{has_role(manager,user_id,0)// Admin is always bit 0
}publicfunhas_operator_role(manager: &RoleManager,user_id: u64): bool{has_role(manager,user_id,1)// Operator is always bit 1
}}modulesecure::fees{constMAX_FEE_BPS: u64=10000;constE_OVERFLOW: u64=1;constE_INSUFFICIENT_BALANCE: u64=2;publicstructFeeConfighaskey{id: UID,fee_bps: u64,}/// SECURE: Check for overflow before arithmetic
publicfuncalculate_fee(config: &FeeConfig,amount: u64): u64{// Use u128 for intermediate calculation to prevent overflow
letamount_128=(amountasu128);letfee_bps_128=(config.fee_bpsasu128);letmax_fee_bps_128=(MAX_FEE_BPSasu128);letresult_128=(amount_128*fee_bps_128)/max_fee_bps_128;// Safe to downcast since result <= amount
(result_128asu64)}/// Alternative: Use checked arithmetic with explicit handling
publicfuncalculate_fee_checked(config: &FeeConfig,amount: u64): Option<u64>{// Check if multiplication would overflow
if(amount>0&&config.fee_bps>18446744073709551615/amount){returnoption::none()};option::some((amount*config.fee_bps)/MAX_FEE_BPS)}/// SECURE: Explicit underflow check with meaningful error
publicfunwithdraw(balance: &mutu64,amount: u64){assert!(*balance>=amount,E_INSUFFICIENT_BALANCE);*balance=*balance-amount;}}