Why arithmetic checks cost gas

Before Solidity 0.8, arithmetic overflow could silently wrap. Since 0.8, the compiler inserts checks around operations like +, -, *, and ++. If the result would overflow or underflow, the transaction reverts.

That behavior is safer, but it means every arithmetic operation includes extra logic. In tight loops, counters, or accounting code where the valid range is already proven by invariants, those checks become unnecessary overhead.

The unchecked block tells the compiler to skip overflow and underflow checks for the enclosed arithmetic operations.

unchecked {
    total += amount;
}

Use it only when you can prove the arithmetic cannot overflow or underflow under valid execution paths.


When unchecked is a good fit

unchecked is most useful when the code already enforces a strict bound.

Common safe scenarios

  • Loop counters that are guaranteed to stop before overflow
  • Decrementing a value after checking it is nonzero
  • Adding values with a known upper limit
  • Internal accounting where totals are bounded by supply, balance, or array length

Typical examples

  • Iterating over uint256 i from 0 to length - 1
  • Decreasing a balance after validating balance >= amount
  • Increasing a counter that cannot reach type(uint256).max in practice, such as a small array index or bounded nonce

The key idea is simple: if the surrounding logic already proves safety, the compiler does not need to prove it again on every operation.


A practical example: loop increments

The most common use of unchecked is in for loops. Solidity checks i++ on every iteration, even when the loop condition already guarantees that i stays within range.

Baseline version

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

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

This is perfectly correct, but i++ incurs an overflow check each time.

Optimized version

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

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

Why this is safe

  • i starts at 0
  • i increases by exactly 1
  • The loop stops when i == length
  • length is a uint256, so the loop cannot run long enough to overflow i in any realistic execution

This pattern is especially useful in functions that process arrays, batches, or fixed-size collections.


A practical example: guarded subtraction

Another common case is decrementing a value after a prior check.

Baseline version

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

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

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

The subtraction is safe because the require ensures the balance is large enough.

Optimized version

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

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

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

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

This saves the compiler from inserting a redundant underflow check.

Important note

Do not move the subtraction into unchecked unless the validation happens first and cannot be bypassed. If future refactoring changes the control flow, the safety guarantee may disappear.


Where unchecked is not appropriate

unchecked is not a general optimization switch. It should not be used just because a function is “hot” or because gas savings sound attractive.

Avoid unchecked when:

  • The arithmetic depends on user input without a strong bound
  • The code path is complex and safety is hard to reason about
  • The value may be reused across multiple branches
  • The operation is part of critical accounting that must never silently wrap

Risky example

unchecked {
    totalSupply += mintedAmount;
}

This is only safe if mintedAmount and totalSupply are bounded by a strict invariant. Otherwise, a wraparound could corrupt supply accounting and break the contract.

In financial contracts, correctness usually matters more than marginal gas savings. If the proof of safety is not obvious, keep the checked arithmetic.


Comparing checked and unchecked arithmetic

PatternSafetyGas costBest use case
Default arithmeticHighHigherGeneral-purpose contract logic
unchecked increment in loopsHigh if boundedLowerArray iteration, batch processing
unchecked subtraction after requireHigh if validated firstLowerWithdrawals, balance updates
unchecked on unbounded user inputLowLowerNot recommended

The table highlights the tradeoff: unchecked reduces gas, but only when the surrounding code makes overflow impossible.


How much gas can it save?

The exact savings depend on the compiler version, optimization settings, and surrounding code. In practice, the benefit is usually modest per operation but meaningful in aggregate.

Best candidates for measurable savings

  • Loops that run many times
  • Batch operations over arrays
  • Repeated counter increments in internal logic
  • Tight accounting paths executed frequently by users

For a single arithmetic operation, the savings may be small. For a loop that runs hundreds of times, removing the check from each iteration can add up.

Rule of thumb

Use unchecked where:

  • the operation is repeated often, and
  • the safety proof is simple and local

If the code is executed rarely, the readability tradeoff may not be worth it.


Best practices for safe usage

1. Keep the unchecked block as small as possible

Do not wrap large sections of code. Limit the block to the exact arithmetic that is safe without checks.

unchecked {
    ++i;
}

This makes the intent clear and reduces the chance of accidental misuse.

2. Prove safety with nearby checks

If subtraction is unchecked, place the validating require immediately before it.

require(balance >= amount, "insufficient balance");
unchecked {
    balance -= amount;
}

This makes the relationship between the check and the arithmetic obvious.

3. Prefer local variables for clarity

When a value is read from storage and then updated, cache it in a local variable first. This improves readability and makes the invariant easier to inspect.

uint256 balance = balances[msg.sender];
require(balance >= amount, "insufficient balance");

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

4. Document the invariant

A short comment can help future maintainers understand why the code is safe.

// Safe: i is bounded by values.length.
unchecked {
    ++i;
}

5. Test boundary conditions

Write tests for:

  • zero values
  • maximum expected values
  • empty arrays
  • exact boundary cases where the require passes or fails

This is especially important when unchecked is used in code that may evolve over time.


A real-world pattern: batch processing

Batch functions are a strong candidate for unchecked because they often combine loop iteration with repeated arithmetic.

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

contract Airdrop {
    mapping(address => uint256) public claimed;

    function claimBatch(address[] calldata recipients, uint256[] calldata amounts) external {
        require(recipients.length == amounts.length, "length mismatch");

        uint256 length = recipients.length;
        for (uint256 i = 0; i < length; ) {
            address recipient = recipients[i];
            uint256 amount = amounts[i];

            require(claimed[recipient] == 0, "already claimed");
            claimed[recipient] = amount;

            unchecked {
                ++i;
            }
        }
    }
}

Why this pattern works well

  • The loop counter is bounded by length
  • The increment is trivial and safe
  • The function may execute many iterations, so the per-iteration savings matter
  • The business logic remains readable

If the batch size is also capped by design, the safety argument becomes even stronger.


Common mistakes

Mistake 1: wrapping too much code

unchecked {
    require(balance >= amount, "insufficient");
    balance -= amount;
    someOtherFunction();
}

This is too broad. Only the subtraction needs unchecked arithmetic. Keep unrelated logic outside the block.

Mistake 2: assuming a check elsewhere still applies

If a value is modified between the check and the arithmetic, the proof may no longer hold.

require(balance >= amount, "insufficient");
balance = adjust(balance);
unchecked {
    balance -= amount; // may no longer be safe
}

Mistake 3: using unchecked for “performance” without measurement

Not every optimization is worthwhile. If the function is rarely called, the readability cost may outweigh the gas savings.

Mistake 4: using unchecked in public-facing accounting without a clear invariant

For token balances, supply tracking, and fee logic, silent wraparound can be catastrophic. Only use unchecked when the invariant is simple, enforced, and well-tested.


Practical decision guide

Use the following checklist before adding unchecked:

  1. Is the arithmetic operation provably safe?
  2. Is the proof local and easy to read?
  3. Is the operation executed frequently enough to matter?
  4. Is the unchecked block minimal?
  5. Are tests covering boundary conditions?

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


Conclusion

unchecked arithmetic is one of the simplest ways to reduce gas in Solidity 0.8+, but it should be applied surgically. The best use cases are loop counters and guarded arithmetic where the contract already enforces strict bounds.

Treat unchecked as a precision tool, not a blanket optimization. When used carefully, it can improve efficiency without weakening correctness.

Learn more with useful resources