How to Avoid Arithmetic Mistakes in Solidity Contracts

Article author

Arithmetic mistakes in Solidity smart contracts occur when token, share, or price calculations use the wrong formula, decimal scale, rounding direction, or numeric assumption, allowing users to mint excess claims, withdraw more value than they deposited, or cause accounting invariants to fail. The remedy is to define units explicitly, use full-precision bounded arithmetic, and test the economic invariants that the formula must preserve.

The consequences are material. Cetus CLMM lost $223.0M on Sui on 2025-05-22 in an incident classified as an Arithmetic Error involving token and share accounting. Arithmetic safety is therefore not merely about preventing Solidity overflow reverts. It is about ensuring every minted share represents a defensible claim on assets.

What are arithmetic mistakes in Solidity and how do attacks work?

Arithmetic mistakes in Solidity are defects in calculations that translate deposits, withdrawals, balances, prices, fees, or liquidity into economic claims. An attacker exploits the mismatch between the intended accounting model and the implemented formula, often by choosing transaction order, asset amounts, or pool states that make an incorrect conversion favour them.

In modern Solidity, arithmetic overflow and underflow revert by default. That protection is useful, but it does not prove a formula is economically correct. A calculation can remain within uint256 bounds while minting too many vault shares, charging too little collateral, or rounding a withdrawal in the user’s favour.

A typical excess-share attack follows this sequence:

  1. A vault has existing assets and shares, so each share has a non-unit price.
  2. The vault receives yield, fees, donations, or a rebasing balance increase.
  3. The share price rises because total assets increased while total shares did not.
  4. An attacker deposits assets through a formula that assumes one share always equals one asset.
  5. The vault mints more shares than the deposit should buy at the current price.
  6. The attacker redeems those shares for a disproportionate fraction of pooled assets.

The core invariant is simple: after a deposit, the new user’s ownership fraction should correspond to the value they contributed. If a deposit worth 1% of pool assets produces more than roughly 1% of outstanding shares, existing holders are diluted.

Which Solidity patterns cause token and share-accounting errors?

Token and share-accounting errors usually come from mixing units, truncating intermediate values, or treating a changing share price as fixed. The highest-risk functions are deposit, mint, withdraw, redeem, borrowing limits, fee settlement, liquidity calculations, and reward distribution.

A dangerous pattern is minting shares directly from deposited asset units:

shares = assets * 1e18 / assetScale;

That conversion may be appropriate only for the first deposit, when one share is deliberately defined as one normalized asset unit. Once a vault earns yield or suffers loss, deposits must use the current exchange rate:

shares = assets * totalShares / totalAssets;

Even that expression needs careful treatment. Multiplying first can overflow if placed inside unchecked, while dividing first can lose precision. Decimal normalization creates another source of error. USDC-style assets commonly use fewer decimals than an 18-decimal share token, and a protocol must consistently distinguish raw token units from normalized accounting units.

Common causes include:

  • Hard-coding 1e18 for an asset with a different decimal precision.
  • Minting shares at a fixed one-to-one rate after the share price changes.
  • Dividing before multiplying and silently losing meaningful precision.
  • Rounding deposits up or withdrawals up, creating cumulative value leakage.
  • Using unchecked around user-controlled values or scaling factors.
  • Assuming token balances cannot change outside normal deposit and withdrawal flows.
  • Forgetting that fee-on-transfer, rebasing, and yield-bearing tokens can invalidate cached accounting.

How do you fix arithmetic mistakes in Solidity share calculations?

To fix arithmetic mistakes in Solidity, normalize asset units once, calculate shares against the current pool ratio, use full-precision multiplication and division, and choose rounding that cannot transfer value from existing holders to a new depositor. The contract must also treat its asset balance and its share supply as linked accounting state.

The vulnerable vault below accepts a token with configurable decimals. It mints normalized shares at a fixed rate even after reportYield increases assets. A depositor can receive too many shares after yield has accrued, then redeem those shares against the larger pool.

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

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
}

contract VulnerableShareVault {
    IERC20 public immutable asset;
    uint8 public immutable assetDecimals;
    uint256 public totalAssets;
    uint256 public totalShares;
    mapping(address => uint256) public shares;

    constructor(address asset_, uint8 decimals_) {
        asset = IERC20(asset_);
        assetDecimals = decimals_;
    }

    function deposit(uint256 assets) external {
        require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");

        uint256 scale = 10 ** assetDecimals;
        // VULNERABLE: ignores the current price of existing shares.
        uint256 minted = (assets * 1e18) / scale;

        totalAssets += assets;
        totalShares += minted;
        shares[msg.sender] += minted;
    }

    function reportYield(uint256 assets) external {
        require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");
        totalAssets += assets;
    }

    function withdraw(uint256 shareAmount) external {
        uint256 assets = (shareAmount * totalAssets) / totalShares;
        shares[msg.sender] -= shareAmount;
        totalShares -= shareAmount;
        totalAssets -= assets;
        require(asset.transfer(msg.sender, assets), "transfer failed");
    }
}

The fixed version converts raw asset units into 18-decimal accounting units and mints shares at the current exchange rate. It uses OpenZeppelin’s full-precision Math.mulDiv, floors both deposits and withdrawals, and bounds supported decimal values.

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

import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
}

contract FixedShareVault {
    IERC20 public immutable asset;
    uint256 public immutable assetScale;
    uint256 public totalAssets;
    uint256 public totalShares;
    mapping(address => uint256) public shares;

    constructor(address asset_, uint8 decimals_) {
        require(decimals_ <= 18, "unsupported decimals");
        asset = IERC20(asset_);
        assetScale = 10 ** decimals_;
    }

    function deposit(uint256 assets) external {
        require(assets > 0, "zero assets");
        require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");

        uint256 normalizedAssets = Math.mulDiv(assets, 1e18, assetScale);
        uint256 normalizedPool = Math.mulDiv(totalAssets, 1e18, assetScale);

        // FIX: price shares against the pre-deposit pool ratio.
        uint256 minted = totalShares == 0
            ? normalizedAssets
            : Math.mulDiv(normalizedAssets, totalShares, normalizedPool);

        require(minted > 0, "deposit too small");
        totalAssets += assets;
        totalShares += minted;
        shares[msg.sender] += minted;
    }

    function withdraw(uint256 shareAmount) external {
        uint256 assets = Math.mulDiv(shareAmount, totalAssets, totalShares);
        shares[msg.sender] -= shareAmount;
        totalShares -= shareAmount;
        totalAssets -= assets;
        require(asset.transfer(msg.sender, assets), "transfer failed");
    }
}

If your protocol prices deposits, withdrawals, collateral, or liquidity through custom arithmetic, a smart-contract audit and security assessment can trace unit conversions and economic invariants across the full transaction lifecycle.

How can developers stop making arithmetic errors?

Developers stop making arithmetic errors by specifying accounting units before implementing formulas, applying conservative rounding, and treating every conversion as a security boundary. A formula should state what each input represents, its decimal precision, its permitted range, and who benefits from rounding.

Technique What it stops Gas or complexity cost Limitation
Explicit unit normalization Mixing raw token units with 18-decimal share units Extra multiplication or division Requires a documented decimal policy
Math.mulDiv Overflow and precision loss in a * b / c More gas than simple operators Does not validate the economic formula
Floor deposits and withdrawals Over-minting and over-redemption from rounding May create dust balances Dust needs a recovery policy
Minimum share mint threshold Deposits that round to zero shares One comparison Must not block legitimate small users unexpectedly
Bounded inputs and decimals Unsafe exponentiation and extreme user inputs Validation checks Bounds must match supported assets
Invariant tests State transitions that violate pool accounting Test-writing effort Invariants must cover adversarial sequences

Use floor rounding where the alternative would issue value the pool cannot support. For example, a vault normally rounds deposited shares down and redeemed assets down. If a product needs user-favourable rounding, it should explicitly fund the resulting subsidy rather than silently imposing it on other depositors.

Security insight: Under Soken methodology, arithmetic review starts with units and conservation laws, not operators. A checked multiplication can still be exploitable if the result represents the wrong asset, share, price, or ownership claim.

What real incidents show the impact of arithmetic errors?

Arithmetic Error incidents demonstrate that token and share accounting is a protocol-wide security property rather than a local code-quality issue. The affected logic can be in AMM reserves, lending rewards, vault shares, liquidity calculations, or asset conversion routines.

Protocol Date Amount lost Chain
Cetus CLMM 2025-05-22 $223.0M Sui
Compound V2 2021-09-29 $147.0M Ethereum
Uranium Finance 2021-04-28 $57.2M BSC
ValueDefi 2021-05-07 $11.0M BSC
KiiChain 2026-08-22 $9.7M KiiChain
Truebit 2026-01-08 $26.4M Ethereum
Notional V2 2026-09-04 $1.7M Ethereum
USM 2026-08-09 $136K Ethereum

These incidents share a critical characteristic: a calculation governed the distribution or valuation of on-chain value. The failure was not necessarily an unchecked uint256 addition. In token and share accounting, the vulnerable condition is often an incorrect relationship between reserves, supply, price, and user claims.

DefiLlama records 35 smart-contract Arithmetic Error incidents from 2020 through 2026, with combined losses of $561.0M. For protocol teams, that history supports reviewing accounting paths with the same priority as access control and external-call handling.

How do you detect arithmetic mistakes before deployment?

Arithmetic mistakes are best detected by reviewing state transitions and testing economic properties across many sequences, not by checking isolated formulas manually. Static analysis can identify risky operators and unchecked blocks, while fuzzing and invariant tests establish whether the accounting model holds under deposits, withdrawals, yield, loss, and unusual decimal values.

Use this focused review checklist:

  1. List every asset, share, price, rate, and fee variable with its unit and decimals.
  2. Verify every deposit uses the pre-deposit exchange rate.
  3. Verify every withdrawal uses a rate consistent with the shares being burned.
  4. Search for raw *, /, unchecked, and hard-coded scale constants in accounting paths.
  5. Test assets with differing decimal precision and deposits near minimum sizes.
  6. Confirm rounding cannot mint unsupported shares or release unsupported assets.
  7. Model balance changes from rebasing, donations, fees, and yield before each user action.
  8. Check that total user claims cannot exceed assets available for redemption.

Slither can flag dangerous arithmetic patterns, unchecked blocks, and suspicious precision operations, but it cannot determine whether a share-price formula is economically sound. Foundry fuzzing is more effective when combined with assertions such as totalAssets covering all redeemed claims and a depositor being unable to profit from an immediate deposit-withdraw cycle absent legitimate yield.

Invariant tests should generate multi-user sequences: deposit, yield report, withdrawal, partial withdrawal, donation, and repeated small deposits. Soken review practice also compares the intended mathematical model with each state update, because a correct formula applied before rather than after a state change can still misprice shares.

Arithmetic safety is the discipline of preserving economic truth across every conversion. A vault can use Solidity 0.8 checked arithmetic and still be vulnerable if it mints claims at the wrong price, assumes the wrong decimals, or rounds value outward.

Start by writing the unit and invariant for every accounting variable, then encode those invariants in Foundry tests before adding new asset types or pricing paths. For deeper research on Solidity security patterns, visit the Soken Hub.

Article author

Frequently Asked Questions

What are arithmetic mistakes in Solidity smart contracts?

Arithmetic mistakes in Solidity are defects in calculations for deposits, withdrawals, balances, prices, fees, liquidity, tokens, or shares. Arithmetic mistakes in Solidity can create excess claims, excessive withdrawals, or accounting invariant failures when formulas, decimal scales, rounding directions, or numeric assumptions are wrong.

How can arithmetic mistakes let users withdraw too much?

Arithmetic mistakes in Solidity can let users mint excess claims or withdraw more value than they deposited when token, share, or price calculations apply an incorrect formula, decimal scale, rounding direction, or numeric assumption. The calculation then assigns an economic claim that the assets do not support.

How do I prevent arithmetic mistakes in Solidity?

Arithmetic mistakes in Solidity are prevented by defining the units of every calculation explicitly, using full-precision bounded arithmetic, and testing the economic invariants the formula must preserve. These controls address wrong formulas, decimal scales, rounding directions, and numeric assumptions that can distort token, share, or price claims.

Are overflow reverts enough to stop arithmetic mistakes in Solidity?

Overflow reverts alone do not eliminate arithmetic mistakes in Solidity. Arithmetic safety also requires correct formulas, explicit units, appropriate decimal scales, correct rounding direction, and sound numeric assumptions. A calculation can avoid an overflow revert yet still give a minted share or withdrawal an indefensible claim on assets.

What does the Cetus CLMM loss teach about arithmetic errors?

Cetus CLMM lost $223.0M on Sui in an incident classified as an Arithmetic Error involving token and share accounting. The Cetus CLMM incident shows that arithmetic safety is not limited to preventing Solidity overflow reverts, because every minted share must represent a defensible claim on assets.

Chat