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#
TOML
[profile.release]
codegen-units = 1
opt-level = "z"
lto = true
debug = false
panic = "abort"
# BUG: overflow-checks missing -> release builds wrap silentlyRust
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#
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