Near Learn

Overflow, panics and require!

Rust integer overflow in NEAR contracts: keep overflow-checks on in release, use checked math, and understand what a panic reverts (and what it doesn’t).

Advanced5 min read3-question check

Solidity 0.8+ reverts on overflow by default. Rust does too — in debug builds only. A plain cargo build --release compiles a - b to wrapping arithmetic unless you opt in, and contracts are always deployed as release builds. One profile setting is the difference between a clean panic and a balance of 2¹²⁸ − 1.

The bug#

Vulnerable — Cargo.toml without overflow checks
TOML
[profile.release]
codegen-units = 1
opt-level = "z"
lto = true
debug = false
panic = "abort"
# BUG: overflow-checks missing -> release builds wrap silently
Vulnerable — unchecked subtraction that wraps in release
Rust
pub fn transfer(&mut self, to: AccountId, amount: U128) {
    let from = env::predecessor_account_id();
    let from_balance = self.balances.get(&from).copied().unwrap_or(0);
    // with overflow-checks off, 5 - 10 wraps to a huge u128
    self.balances.insert(from, from_balance - amount.0);
    let to_balance = self.balances.get(&to).copied().unwrap_or(0);
    self.balances.insert(to, to_balance + amount.0);
}

The fix#

Fixed — overflow-checks on, plus explicit checked math with clear messages
Rust
// Cargo.toml
// [profile.release]
// overflow-checks = true   <- keep this (the cargo-near template sets it)

pub fn transfer(&mut self, to: AccountId, amount: U128) {
    let from = env::predecessor_account_id();
    // without this, both reads below see the same balance and the second
    // insert overwrites the first: a self-transfer would mint tokens
    require!(from != to, "cannot transfer to yourself");

    let from_balance = self.balances.get(&from).copied().unwrap_or(0);
    let new_from = from_balance
        .checked_sub(amount.0)
        .unwrap_or_else(|| env::panic_str("insufficient balance"));

    let to_balance = self.balances.get(&to).copied().unwrap_or(0);
    let new_to = to_balance
        .checked_add(amount.0)
        .unwrap_or_else(|| env::panic_str("balance overflow"));

    self.balances.insert(from, new_from);
    self.balances.insert(to, new_to);
}

What a panic does (and doesn’t) undo#

Check yourself

3 questions · progress saved in this browser

  1. 1.A contract is built with cargo build --release and no overflow-checks setting. A user with balance 5 transfers 10 via balance - amount on u128. What happens?
  2. 2.A callback panics halfway through. Which state changes are reverted?
  3. 3.Which is the best way to handle an insufficient-balance withdrawal of a ledger balance?