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:
- A vault has existing assets and shares, so each share has a non-unit price.
- The vault receives yield, fees, donations, or a rebasing balance increase.
- The share price rises because total assets increased while total shares did not.
- An attacker deposits assets through a formula that assumes one share always equals one asset.
- The vault mints more shares than the deposit should buy at the current price.
- 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
1e18for 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
uncheckedaround 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:
- List every asset, share, price, rate, and fee variable with its unit and decimals.
- Verify every deposit uses the pre-deposit exchange rate.
- Verify every withdrawal uses a rate consistent with the shares being burned.
- Search for raw
*,/,unchecked, and hard-coded scale constants in accounting paths. - Test assets with differing decimal precision and deposits near minimum sizes.
- Confirm rounding cannot mint unsupported shares or release unsupported assets.
- Model balance changes from rebasing, donations, fees, and yield before each user action.
- 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.