General logic errors in Move contracts include PTB (Programmable Transaction Block) reordering effects, incorrect mutation order, fee miscalculations, and state inconsistencies. These bugs are often subtle and can lead to fund loss or protocol manipulation.
Risk Level
Medium to Critical — Varies based on the specific logic error.
Invariant violations — Protocol rules not enforced consistently
Edge cases — Zero values, empty collections, boundary conditions
Vulnerable Examples
Example 1: Incorrect Mutation Order
modulevulnerable::lending{usesui::object::UID;usesui::coin::{Self,Coin};usesui::sui::SUI;publicstructLendingPoolhaskey{id: UID,total_deposits: u64,total_borrows: u64,interest_rate: u64,}/// VULNERABLE: Interest calculated on old balance
publicentryfundeposit(pool: &mutLendingPool,payment: Coin<SUI>,){letamount=coin::value(&payment);// WRONG: Accruing interest AFTER updating deposits
// New deposit earns interest it shouldn't
pool.total_deposits=pool.total_deposits+amount;// Interest calculated on inflated total
accrue_interest(pool);// ... store payment
}/// VULNERABLE: Withdraw before interest accrual
publicentryfunwithdraw(pool: &mutLendingPool,amount: u64,ctx: &mutTxContext){// WRONG: Withdrawing before accruing interest
// User avoids paying accumulated interest
pool.total_deposits=pool.total_deposits-amount;accrue_interest(pool);// Too late!
}}
Example 2: Fee Calculation Errors
modulevulnerable::exchange{constFEE_BPS: u64=30;// 0.3%
constBPS_DENOMINATOR: u64=10000;publicstructExchangehaskey{id: UID,accumulated_fees: u64,}/// VULNERABLE: Precision loss in fee calculation
publicfuncalculate_fee(amount: u64): u64{// For small amounts, this can round to 0
// amount = 100, fee = 100 * 30 / 10000 = 0
amount*FEE_BPS/BPS_DENOMINATOR}/// VULNERABLE: Fee-on-fee calculation
publicfunswap_with_fee(exchange: &mutExchange,input_amount: u64,): u64{letfee=calculate_fee(input_amount);letnet_input=input_amount-fee;// Calculate output
letoutput=calculate_output(net_input);// WRONG: Taking fee again on output!
letoutput_fee=calculate_fee(output);letnet_output=output-output_fee;// User pays fee twice
exchange.accumulated_fees=exchange.accumulated_fees+fee+output_fee;net_output}/// VULNERABLE: Integer division ordering
publicfuncalculate_share(amount: u64,user_balance: u64,total_balance: u64): u64{// WRONG: Division before multiplication loses precision
// If user_balance < total_balance, this might return 0
amount*(user_balance/total_balance)// Should be: (amount * user_balance) / total_balance
}}
Example 3: State Invariant Violations
modulevulnerable::amm{publicstructPoolhaskey{id: UID,reserve_a: u64,reserve_b: u64,k: u64,// Constant product: reserve_a * reserve_b = k
}/// VULNERABLE: k not updated after liquidity change
publicentryfunadd_liquidity(pool: &mutPool,amount_a: u64,amount_b: u64,){pool.reserve_a=pool.reserve_a+amount_a;pool.reserve_b=pool.reserve_b+amount_b;// FORGOT to update k!
// pool.k = pool.reserve_a * pool.reserve_b;
// Now k invariant is broken
// Swaps will use stale k value
}/// VULNERABLE: No check that k is maintained after swap
publicentryfunswap_a_for_b(pool: &mutPool,amount_a_in: u64,): u64{letnew_reserve_a=pool.reserve_a+amount_a_in;// Calculate output to maintain k
// But rounding might break the invariant
letamount_b_out=pool.reserve_b-(pool.k/new_reserve_a);pool.reserve_a=new_reserve_a;pool.reserve_b=pool.reserve_b-amount_b_out;// No assertion that k is still valid!
// assert!(pool.reserve_a * pool.reserve_b >= pool.k, E_K_VIOLATED);
amount_b_out}}
Secure Examples
Secure Lending Pool
modulesecure::lending{usesui::object::UID;usesui::coin::{Self,Coin};usesui::sui::SUI;usesui::clock::{Self,Clock};publicstructLendingPoolhaskey{id: UID,total_deposits: u64,total_borrows: u64,interest_rate_per_ms: u64,last_update_ms: u64,accumulated_interest: u64,}/// SECURE: Always accrue interest first
funaccrue_interest_internal(pool: &mutLendingPool,clock: &Clock){letnow=clock::timestamp_ms(clock);letelapsed=now-pool.last_update_ms;if(elapsed>0){letinterest=(pool.total_borrowsasu128)*(pool.interest_rate_per_msasu128)*(elapsedasu128)/1_000_000_000_000;pool.accumulated_interest=pool.accumulated_interest+(interestasu64);pool.last_update_ms=now;}}/// SECURE: Interest accrued before state change
publicentryfundeposit(pool: &mutLendingPool,payment: Coin<SUI>,clock: &Clock,){// FIRST: Accrue interest on existing state
accrue_interest_internal(pool,clock);// THEN: Update deposits
letamount=coin::value(&payment);pool.total_deposits=pool.total_deposits+amount;// ... store payment
}/// SECURE: Consistent ordering
publicentryfunwithdraw(pool: &mutLendingPool,amount: u64,clock: &Clock,ctx: &mutTxContext){// FIRST: Accrue interest
accrue_interest_internal(pool,clock);// THEN: Process withdrawal
assert!(pool.total_deposits>=amount,E_INSUFFICIENT_LIQUIDITY);pool.total_deposits=pool.total_deposits-amount;}}
Secure Fee Calculations
modulesecure::exchange{constFEE_BPS: u64=30;constBPS_DENOMINATOR: u64=10000;constMIN_FEE: u64=1;// Minimum fee to prevent zero-fee exploits
/// SECURE: Handle precision loss
publicfuncalculate_fee(amount: u64): u64{letfee=(amount*FEE_BPS)/BPS_DENOMINATOR;// Ensure minimum fee on non-zero amounts
if(amount>0&&fee==0){MIN_FEE}else{fee}}/// SECURE: Use u128 for intermediate calculations
publicfuncalculate_share(amount: u64,user_balance: u64,total_balance: u64): u64{if(total_balance==0){return0};// Use u128 to prevent overflow and maintain precision
letresult=((amountasu128)*(user_balanceasu128))/(total_balanceasu128);(resultasu64)}/// SECURE: Single fee, clear calculation
publicfunswap_with_fee(exchange: &mutExchange,input_amount: u64,): u64{assert!(input_amount>0,E_ZERO_INPUT);letfee=calculate_fee(input_amount);letnet_input=input_amount-fee;// Calculate output — no additional fee
letoutput=calculate_output(net_input);exchange.accumulated_fees=exchange.accumulated_fees+fee;output}}
Secure AMM with Invariant Checks
modulesecure::amm{constE_K_VIOLATED: u64=1;constE_ZERO_LIQUIDITY: u64=2;constE_SLIPPAGE: u64=3;publicstructPoolhaskey{id: UID,reserve_a: u64,reserve_b: u64,}/// Calculate k from current reserves
funget_k(pool: &Pool): u128{(pool.reserve_aasu128)*(pool.reserve_basu128)}/// SECURE: Update reserves and verify invariant
publicentryfunadd_liquidity(pool: &mutPool,amount_a: u64,amount_b: u64,){assert!(amount_a>0&&amount_b>0,E_ZERO_LIQUIDITY);// For existing pool, require proportional deposit
if(pool.reserve_a>0){letexpected_b=((amount_aasu128)*(pool.reserve_basu128))/(pool.reserve_aasu128);// Allow small deviation for rounding
assert!(amount_b>=(expected_basu64)-1&&amount_b<=(expected_basu64)+1,E_SLIPPAGE);};pool.reserve_a=pool.reserve_a+amount_a;pool.reserve_b=pool.reserve_b+amount_b;}/// SECURE: Verify k maintained after swap
publicentryfunswap_a_for_b(pool: &mutPool,amount_a_in: u64,min_b_out: u64,): u64{letk_before=get_k(pool);letnew_reserve_a=pool.reserve_a+amount_a_in;// Calculate output using u128 for precision
letnew_reserve_b=(k_before/(new_reserve_aasu128))asu64;letamount_b_out=pool.reserve_b-new_reserve_b;// Slippage check
assert!(amount_b_out>=min_b_out,E_SLIPPAGE);// Update reserves
pool.reserve_a=new_reserve_a;pool.reserve_b=new_reserve_b;// CRITICAL: Verify k invariant (with tolerance for rounding)
letk_after=get_k(pool);assert!(k_after>=k_before,E_K_VIOLATED);amount_b_out}}
Logic Error Prevention Patterns
Pattern 1: Check-Effects-Interactions
publicentryfunsecure_operation(state: &mutState,input: u64){// 1. CHECKS - Validate all preconditions
assert!(input>0,E_ZERO_INPUT);assert!(state.balance>=input,E_INSUFFICIENT);// 2. EFFECTS - Update state
state.balance=state.balance-input;state.processed=state.processed+1;// 3. INTERACTIONS - External calls last
emit_event(...);}
Pattern 2: Invariant Assertions
/// Always assert invariants at function end
publicentryfunmodify_state(state: &mutState,...){// ... make changes
// Assert invariants before returning
assert_invariants(state);}funassert_invariants(state: &State){assert!(state.total==state.a+state.b+state.c,E_TOTAL_MISMATCH);assert!(state.balance>=state.minimum_required,E_UNDERCOLLATERALIZED);}
Pattern 3: Use Receipts for Multi-Step Operations
/// Hot potato pattern for operations that must complete
publicstructOperationReceipt{expected_outcome: u64,}publicfunstart_operation(state: &mutState,amount: u64): OperationReceipt{state.locked=state.locked+amount;OperationReceipt{expected_outcome: calculate_expected(amount)}}publicfunfinish_operation(state: &mutState,receipt: OperationReceipt,actual: u64){letOperationReceipt{expected_outcome}=receipt;assert!(actual>=expected_outcome,E_UNEXPECTED_OUTCOME);// Receipt consumed — operation must complete
}
Testing Checklist
Test all functions with zero values
Test with maximum (u64::MAX) values
Verify interest/fee calculations across time boundaries
Test invariants hold after every state-changing operation
Test PTB with reordered calls
Verify precision in all arithmetic with small and large values