Skip to content

Latest commit

 

History

History
108 lines (82 loc) · 3.43 KB

File metadata and controls

108 lines (82 loc) · 3.43 KB

S003 — Unchecked Arithmetic

  • Category: arithmetic
  • Severity: Medium
  • Rule name: arithmetic_overflow

What it detects

S003 flags unchecked arithmetic operations that can overflow, underflow, or panic on a zero divisor:

  • Binary operators: +, -, *, /, %.
  • Compound assignment: +=, -=, *=, /=, %=.
  • Custom math methods and calls: .mul_div(), .fixed_point_mul(), .fixed_point_div(), .div_ceil() and their function-style equivalents.

It does not flag .checked_* / .saturating_* methods, comparison and bitwise operators, string concatenation, array-index arithmetic (buf[i + 1]), or code under #[test] / #[cfg(test)]. Findings are deduplicated to at most one per (function, operator) pair.

Why it matters

In Rust, integer overflow panics in debug builds and wraps silently in release builds. For a contract handling balances, either outcome is unacceptable: a panic locks state, and silent wraparound produces incorrect balances, unauthorized minting, or loss-of-funds rounding.

Vulnerable example

#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env};

#[contracttype]
pub enum DataKey {
    Balance(Address),
}

#[contract]
pub struct Token;

#[contractimpl]
impl Token {
    pub fn mint(env: Env, to: Address, amount: i128) {
        let current: i128 = env
            .storage()
            .persistent()
            .get(&DataKey::Balance(to.clone()))
            .unwrap_or(0);
        // S003: unchecked addition can overflow.
        let new_balance = current + amount;
        env.storage()
            .persistent()
            .set(&DataKey::Balance(to), &new_balance);
    }
}

Safe example

#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env};

#[contracttype]
pub enum DataKey {
    Balance(Address),
}

#[contract]
pub struct Token;

#[contractimpl]
impl Token {
    pub fn mint(env: Env, to: Address, amount: i128) {
        let current: i128 = env
            .storage()
            .persistent()
            .get(&DataKey::Balance(to.clone()))
            .unwrap_or(0);
        // checked_add returns None on overflow.
        let new_balance = current
            .checked_add(amount)
            .expect("mint: balance overflow");
        env.storage()
            .persistent()
            .set(&DataKey::Balance(to), &new_balance);
    }
}

CVSS-style risk rating

  • Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:L
  • Base score: 5.9
  • Rating: Medium

Exploitation usually requires crafted boundary inputs (higher attack complexity), but a successful overflow can corrupt balances (high integrity impact).

How to fix

  1. Replace unchecked operators with .checked_add / .checked_sub / .checked_mul / .checked_div / .checked_rem and handle the None case.
  2. Where clamping is acceptable for non-financial values, use the .saturating_* variants.
  3. For custom fixed-point math, use checked wrappers (e.g. .checked_mul_div).
  4. Cast to a wider integer type before arithmetic when widening makes the operation provably safe.

Related rules

Related rules: S002, S011

References