What unchecked Does

Inside an unchecked block, Solidity skips automatic overflow and underflow checks for arithmetic on uint and int types. This applies to operations such as:

  • addition
  • subtraction
  • multiplication
  • division by constants is still checked for division by zero
  • increment and decrement
  • compound assignments like += and -=

Outside unchecked, the compiler inserts runtime checks and reverts on overflow or underflow.

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Counter {
    uint256 public count;

    function increment() external {
        unchecked {
            count += 1;
        }
    }
}

In this example, the contract assumes count will never reach type(uint256).max. If that assumption is valid, the unchecked block avoids the overflow check and saves gas.


When unchecked Is Appropriate

unchecked is useful when you can prove the arithmetic cannot overflow or underflow based on surrounding logic.

Common cases include:

  • loop counters with known bounds
  • arithmetic after explicit range checks
  • decrementing a value only after verifying it is nonzero
  • accumulator patterns where the maximum possible value is bounded
  • performance-sensitive code paths called frequently

Typical use cases

ScenarioWhy unchecked helpsSafety condition
for loop incrementsAvoids repeated overflow checksLoop bound guarantees the counter cannot overflow
balance -= amount after validationSaves gas on a verified subtractionamount <= balance already checked
Fixed-size iterationsReduces overhead in hot pathsIndex stays within a known range
Packed accounting logicMinimizes repeated arithmetic checksUpper bounds are enforced elsewhere

The key idea is simple: use unchecked only when the contract logic already guarantees correctness.


A Practical Example: Loop Optimization

A common and safe use of unchecked is incrementing a loop counter when the loop condition itself guarantees termination before overflow.

Safe version

function sum(uint256[] memory values) external pure returns (uint256 total) {
    for (uint256 i = 0; i < values.length; i++) {
        total += values[i];
    }
}

This is correct, but the compiler checks i++ on every iteration.

Optimized version

function sum(uint256[] memory values) external pure returns (uint256 total) {
    for (uint256 i = 0; i < values.length; ) {
        total += values[i];
        unchecked {
            ++i;
        }
    }
}

Why this is safe:

  • i starts at 0
  • the loop stops when i == values.length
  • array lengths cannot exceed type(uint256).max
  • therefore ++i cannot overflow before the loop ends

This pattern is especially useful in functions that iterate over arrays, sets, or fixed-length data structures.


A Practical Example: Safe Decrement After Validation

Another common pattern is subtracting only after a precondition check.

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Vault {
    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) external {
        uint256 current = balances[msg.sender];
        require(current >= amount, "insufficient balance");

        unchecked {
            balances[msg.sender] = current - amount;
        }
    }
}

Here, the subtraction is safe because current >= amount is checked first. Without unchecked, Solidity would perform the same safety check again during subtraction, which is redundant.

This pattern is common in:

  • token accounting
  • escrow balances
  • reward claims
  • internal credit/debit systems

Best practice

Keep the validation and the arithmetic close together. If the code becomes more complex, future maintainers may miss the safety assumption.


Avoiding Common Mistakes

unchecked is not a general optimization tool. It is a precision instrument. The most common mistakes are subtle and often appear in code that “looks obviously safe” at first glance.

1. Using unchecked without a proof

unchecked {
    total += amount;
}

This is unsafe unless you can prove total + amount cannot overflow. If amount comes from user input and total is unbounded, the operation can wrap around and corrupt state.

2. Moving validation away from the arithmetic

require(amount <= balances[msg.sender], "too much");
doSomethingElse();
unchecked {
    balances[msg.sender] -= amount;
}

If doSomethingElse() can change state or introduce reentrancy, the original validation may no longer be valid. Keep the check and the subtraction tightly coupled.

3. Assuming unchecked disables all safety checks

unchecked only affects arithmetic overflow and underflow checks. It does not disable:

  • require statements
  • array bounds checks
  • division-by-zero checks
  • type conversion restrictions
  • external call failures

That means unchecked is narrower than many developers expect.

4. Using it in code that may be refactored later

A loop that is safe today may become unsafe after a future change to its bounds or inputs. If you use unchecked, document the invariant clearly so later changes do not break it.


Document the Invariant, Not Just the Code

The most important best practice is to explain why the arithmetic is safe. A future reviewer should not need to reconstruct the proof from scratch.

Good example

// Safe because i < values.length and array length fits in uint256.
unchecked {
    ++i;
}

Better example

// Safe because `current >= amount` was checked above and both values are uint256.
unchecked {
    balances[msg.sender] = current - amount;
}

This kind of comment is valuable in audit reviews and maintenance work. It turns a hidden assumption into an explicit contract invariant.


Choosing Between Checked and Unchecked Arithmetic

Not every arithmetic operation deserves optimization. In many contracts, the gas saved by unchecked is too small to justify the added risk.

Use checked arithmetic whenUse unchecked when
Inputs are not tightly boundedYou can prove the result stays in range
The code is rarely executedThe code is in a hot path
Readability matters more than micro-optimizationThe operation is repeated many times
The logic may change frequentlyThe invariant is stable and well documented
Security review should be straightforwardThe proof is simple and local

A good rule: start with checked arithmetic, then optimize only where profiling or code review shows a meaningful benefit.


Patterns That Benefit Most

Some Solidity patterns are especially good candidates for unchecked.

Loop counters

Loop increments are the most common safe optimization. The counter is usually bounded by array length or a fixed iteration count.

Balance updates with prior checks

If a function already validates sufficient balance, the subtraction can often be placed inside unchecked.

Internal accounting with capped totals

Protocols that enforce strict caps on supply, rewards, or allocations may safely use unchecked in internal math after validation.

Fixed-step arithmetic

If a value is incremented by a constant and the maximum number of steps is known, unchecked may be appropriate.


Patterns That Should Usually Stay Checked

Some operations should remain checked unless you have a very strong reason and a formal proof.

User-controlled accumulation

If an attacker can repeatedly increase a value, wrapping can become exploitable.

Cross-function state dependencies

If arithmetic depends on state modified by multiple functions, proving safety becomes harder.

Complex financial logic

In lending, liquidation, or reward distribution code, correctness is more important than a small gas reduction.

Code with evolving requirements

If the logic is likely to change, checked arithmetic is safer and easier to maintain.


Testing unchecked Logic

Because unchecked removes runtime protection, tests become more important. You should verify both the expected path and the boundary conditions.

Recommended tests

  • minimum values
  • maximum values
  • zero values
  • exact boundary values where subtraction becomes valid or invalid
  • loop termination behavior
  • repeated execution near limits

Example test ideas

  • withdrawing the full balance succeeds
  • withdrawing more than the balance reverts before the unchecked subtraction
  • a loop over an empty array returns immediately
  • a loop over a large array completes without overflow in the counter

If you use fuzz testing, target the invariants that justify the unchecked block. For example, fuzz the amount and balance relationship to ensure the precondition always protects the subtraction.


A Maintainable Template

When you do use unchecked, keep the pattern consistent and easy to audit.

function transfer(address to, uint256 amount) external {
    uint256 fromBalance = balances[msg.sender];
    require(fromBalance >= amount, "insufficient balance");

    unchecked {
        balances[msg.sender] = fromBalance - amount;
        balances[to] += amount;
    }
}

This style works well because:

  • validation happens first
  • the unsafe arithmetic is isolated
  • the code is short and readable
  • the invariant is easy to verify

If the recipient balance could overflow in your application, the second line should remain checked or be guarded by a separate cap. Do not assume both sides of a transfer are equally safe.


Practical Guidelines

Use this checklist when deciding whether to introduce unchecked:

  1. Can you state the invariant in one sentence?
  2. Is the invariant enforced immediately before the arithmetic?
  3. Is the code path performance-sensitive enough to justify the change?
  4. Will future maintainers understand why it is safe?
  5. Do tests cover the boundary conditions?
  6. Would a simpler checked version be acceptable?

If the answer to any of these is “no,” keep the checked arithmetic.


Summary

unchecked is one of Solidity’s most useful low-level tools, but it should be applied surgically. It is best suited for arithmetic that is already proven safe by surrounding logic, especially in loops and validated balance updates.

The safest approach is to treat every unchecked block as a small formal claim: the code inside cannot overflow or underflow because the surrounding logic guarantees it. If you can document and test that claim clearly, unchecked can improve gas efficiency without sacrificing correctness.

Learn more with useful resources