Understanding Arithmetic Overflow/Underflows in Rust and Solana Smart Contracts

October 14, 2021

Integer overflow/underflows are surprisingly common in smart contracts, because blockchain applications often compute math over financial data.

Rust is a popular language used in blockchains such as Solana and Polkadot. For many developers, it may be a misconception that Rust is memory-safe so it is free of arithmetic overflow/underflows. This article explains why Rust programs still suffer from arithmetic errors, how these issues affect blockchain security, and how to deal with them in smart contracts.

Why Integer overflow/underflows, fundamentally

Just like in a house where every furniture occupies a space, in computer programs, every piece of data needs a space to store its value. The space is limited. If the value of a certain data (e.g. an account’s balance) after computation exceeds the size of its space, then an overflow/underflow occurs.

In Rust, an unsigned integer can have one of the following types: u8, u16, u32, u64, u128 and usize. The type denotes the number of bits used to store the integer: u8 can hold values between 0 and 255, u16 can hold values between 0 and 65535, and so forth. For example, x = x + 1 — if an u8 integer x is changed to a value outside of its range, say 256 or -1, then an overflow/underflow will occur.

Rust does not prevent Integer overflow/underflow

A bit surprising — Rust behaves differently in debug mode and release mode on Integer overflow/underflows. In debug mode, Rust adds built-in checks for overflow/underflow and panics when an overflow/underflow occurs at runtime.

However, in release (or optimization) mode, Rust silently ignores this behavior by default and computes two’s complement wrapping (e.g., 255+1 becomes 0 for an u8 integer). You can play and see the difference using this link.

Note: The overflow-checks in Rust can be enabled in release mode https://doc.rust-lang.org/cargo/reference/profiles.html#overflow-checks

In other words, an overflow in Rust that appears as panic when debugging may disappear when deployed in production.

While there are good reasons (e.g. performance) for Rust to ignore overflows in release mode, this inconsistent behavior may lead to an illusion of Rust safety on arithmetic operations. This is particularly dangerous for blockchains and smart contracts, which live in adversarial environments.

Common arithmetic errors in Solana programs

Both Solana smart contracts and Solana’s core runtime (the validator code) written in Rust have seen quite a number of arithmetic errors.

A partial list from Github (click links to pull requests):

  • Solana smart contracts overflow/underflows: 1, 2, 3, 4, 5
  • Solana’s core runtime overflow/underflows: 1, 2, 3, 4, 5, 6, 7, 8, 9
Figure 1. integer underflow/overflow fixed in jet-v1

The above shows an integer underflow on total_loan_notes -= note_amount(line 245) and overflow on total_deposit += token_amount (line 246) in the jet-v1 protocol. Both variables are of type u64 . The fixes are straightforward: replace the - with checked_sub and the + with checked_add . These will return None instead of wrapping around on underflow or overflow.

Figure 2. an integer overflow fixed in Solana bpf loader

The above shows an integer overflow in the Solana bpf loader. Both the multiplication num_accounts * size_of::<AccountMeta>() and the addition + data_len can cause overflows. The fixes are to replace the * with saturating_mul and the + with saturating_add. These will saturate values at the numeric bounds instead of overflowing.

Figure 3. an arithmetic error fixed in Solana watchtower

The above shows a slightly different type of arithmetic errors in Solana. The division / between two integers total_current_stake * 100 and total_stake can loss precision due to truncate of fractions. The fix is to change the type of these variables to floating point type f64 . In this way, the result current_stake_percent has also precision of type f64.

How to prevent arithmetic errors in Solana smart contracts

When writing a Solana smart contract in Rust, there are three common ways to deal with arithmetic errors:

  1. Replace+ - * / ** with checked_add checked_sub checked_mul and checked_div checked_pow , respectively . Since the cost of these checks is almost negligible in Solana (compared to high gas fees on Ethereum), it might be a good idea to always have these checks.
  2. Replace + - * ** with saturating_add saturating_sub saturating_mul and saturating_pow , respectively. This ensures the computed value will stay at the numeric bounds instead of overflowing/underflowing.
  3. Alternatively, turn on the overflow-checks in release mode by setting overflow-checks = true under [profile.release] . (credit: freax13)
  4. Cautious on integer division /. To avoid losing precision, change integer to floating point types, as illustrated in an aforementioned code example (Figure 3). However, in general, it is not recommended to deal with money in floating point, because floating point cannot accurately represent most base 10 numbers and errors can accumulate over time. Instead, use a proper decimal type. The article explains nicely the underlying reasons. (credit: Plasma_000 and simukis)

Tool automation

Besides, it’s a no-brainer to have a tool that comprehensively checks for arithmetic errors in all code paths, just like checking spelling mistakes.

Soteria is an advanced tool with built-in support to detect common security pitfalls in Solana smart contracts, including arithmetic overflow/underflows.

For all blogs by Soteria, Please visit https://www.soteria.dev/blogs