Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x04 |
| Mnemonic | DIV |
| Gas | 5 |
| Stack Input | a, b |
| Stack Output | a // b (unsigned integer division, floor) |
| Behavior | Unsigned 256-bit integer division. Result truncates toward zero (floor). Division by zero returns 0 — it does NOT revert. |
Threat Surface
DIV is the EVM’s sole unsigned integer division opcode, and its threat surface is fundamentally different from ADD/MUL overflow. Two properties make it dangerous:
-
Division by zero returns 0 silently. Unlike virtually every other computing environment, the EVM does not trap, revert, or signal on division by zero.
a / 0 == 0for alla. Solidity >= 0.8.0 inserts a pre-check that reverts before the DIV opcode executes, butuncheckedblocks, inline assembly, and raw bytecode bypass this protection entirely. Any code path that reaches DIV with a zero denominator produces a silent wrong answer. -
Truncation toward zero loses precision, and precision loss is directional.
1 / 2 == 0,999 / 1000 == 0, and(a * b) / c != a * (b / c)in integer arithmetic. Every truncation rounds down, systematically favoring one side of a transaction. In DeFi, this directional bias is the root cause of rounding exploits worth hundreds of millions of dollars — attackers structure transactions so that the protocol consistently rounds in the attacker’s favor, extracting value one dust amount at a time or in a single catastrophic manipulation.
These two properties combine to make DIV the most economically exploited arithmetic opcode in DeFi history. While ADD overflow was the dominant vulnerability class from 2016-2020, division rounding and precision loss have become the dominant arithmetic exploit vector in the post-Solidity-0.8.0 era.
Smart Contract Threats
T1: Division by Zero — Silent Failure (Critical)
When b == 0, the EVM’s DIV returns 0 instead of reverting. This means any computation that divides by a potentially-zero value produces a silently incorrect result. Common dangerous patterns:
- Price oracles:
price = totalValue / totalSupplyreturns 0 whentotalSupply == 0, making assets appear free - Exchange rates:
rate = reserveA / reserveBcollapses to 0 if a pool is drained, bypassing solvency checks - Fee calculations:
feePerUnit = totalFees / participantCountreturns 0 when there are no participants, losing all fees - Ratio computations: Any formula where the denominator depends on external state (balances, supply, oracle responses)
Solidity 0.8.0+ inserts ISZERO + JUMPI before every DIV to revert on zero denominator. However:
unchecked { }blocks suppress this check- Inline assembly (
assembly { result := div(a, b) }) uses raw DIV with no check - Vyper, Huff, and other languages have their own policies
- Pre-compiled or hand-optimized bytecode may omit the check for gas savings
T2: Truncation / Rounding Exploitation in Token Vaults (Critical)
The most economically devastating DIV threat in modern DeFi. When vaults calculate shares-per-deposit or assets-per-share, integer division always truncates (rounds down). An attacker who can inflate the exchange rate can force victims’ deposits to round down to zero shares:
// ERC4626-style vault
function deposit(uint256 assets) external returns (uint256 shares) {
shares = assets * totalSupply / totalAssets; // DIV truncates
// If totalAssets is manipulated to be very large relative to totalSupply,
// 'shares' rounds to 0 -- victim deposits real assets and receives nothing
}Attack pattern (vault inflation / donation attack):
- Attacker deposits 1 wei, receives 1 share
- Attacker donates a large amount of underlying assets directly to the vault (not via
deposit) - Exchange rate is now
(1 + donated) / 1— astronomically high - Victim deposits, but
victimAssets * 1 / (1 + donated)truncates to 0 shares - Attacker redeems their 1 share for the victim’s deposited assets plus the original donation
This attack class affects Compound V2, ERC4626 vaults, Yearn vaults, and every share-based accounting system.
T3: Operation Ordering — Divide-First vs. Multiply-First (High)
The order of multiplication and division in integer arithmetic fundamentally changes results:
// WRONG: Divide first, lose precision
uint256 bad = (a / PRECISION) * b; // Truncation before multiplication
// CORRECT: Multiply first, preserve precision
uint256 good = (a * b) / PRECISION; // Full precision until final truncation
// BUT: a * b might overflow if both are largeThis is not an academic distinction. In AMM constant-product calculations, lending protocol interest accrual, and liquidity math, divide-first patterns systematically leak value. The leaked value accumulates and can be extracted by an attacker who structures many small transactions.
Uniswap V3 addresses this with FullMath.mulDiv(), which computes a * b / c using 512-bit intermediate precision, avoiding both overflow and premature truncation.
T4: Price Manipulation via Precision Loss in AMMs (High)
AMM pricing formulas like outputAmount = (reserveOut * amountIn) / (reserveIn + amountIn) lose precision at boundary conditions. When reserves are manipulated to extreme ratios (via flash loans), the truncation in DIV can be amplified:
- Small trades may return 0 output tokens due to truncation, effectively donating to the pool
- Price calculations that feed into oracle reads can be biased by rounding direction
- Batch operations compound rounding errors across many sub-operations
In Balancer V2’s ComposableStablePool, token scaling operations used _upscale() which always rounded down. When invariant (D) calculations accumulated these rounding errors across multiple tokens, the BPT price was artificially deflated, enabling attackers to buy pool tokens cheap and arbitrage the difference.
T5: Dust Amount Extraction via Repeated Rounding (Medium)
When a protocol rounds in the user’s favor on withdrawal (or against the protocol on any operation), an attacker can extract dust by repeating the operation:
// Vulnerable: rounds up on withdrawal
function withdraw(uint256 shares) external returns (uint256 assets) {
assets = (shares * totalAssets + totalSupply - 1) / totalSupply; // Rounds up
// Attacker withdraws 1 share at a time, getting ceil() each time
// vs. withdrawing all at once and getting a single ceil()
}Each individual withdrawal extracts up to 1 wei more than proportional. Over thousands of transactions (feasible on L2s with low gas), the extracted dust becomes significant. The attack is bounded by gas costs on L1 but essentially free on L2 rollups.
T6: Denominator Manipulation for Solvency Bypass (High)
When division-by-zero returns 0 and the result feeds into a solvency or collateral check, the check can be completely bypassed:
// Vulnerable solvency check
function isSolvent(address user) internal view returns (bool) {
uint256 ltv = borrowedAmount * 1e36 / collateralValue;
return ltv <= maxLTV; // If collateralValue manipulation makes ltv = 0, always passes
}If an attacker can manipulate collateralValue to cause the division to produce 0 (either via actual zero or via truncation of a very large denominator), the user appears infinitely solvent and can borrow without adequate collateral.
Protocol-Level Threats
P1: No DoS Vector (Low)
DIV costs a fixed 5 gas with no dynamic component. It operates purely on the stack with no memory, storage, or external access. It cannot be used for gas griefing.
P2: Consensus Safety (Low)
DIV is deterministic: a / b (unsigned floor division) and a / 0 == 0 are unambiguous. All EVM client implementations agree on these semantics. No known consensus divergence has occurred due to DIV.
P3: Compiler Behavioral Differences (Medium)
Different compiler versions and languages handle division differently at the language level, while the underlying DIV opcode is identical:
- Solidity < 0.8.0: No division-by-zero check; raw DIV executes and returns 0
- Solidity >= 0.8.0: Compiler inserts
ISZERO+ revert before DIV (unlessunchecked) - Vyper: Has its own division-by-zero checking strategy
- Huff / raw assembly: No checks unless manually added
Auditors reviewing bytecode must know which compiler produced it to understand whether division-by-zero protections exist.
P4: No State Impact (None)
DIV modifies only the stack. It cannot cause state bloat, storage writes, or memory expansion.
Edge Cases
| Edge Case | Result | Security Implication |
|---|---|---|
a / 0 | Returns 0 | Critical: silent wrong answer; no revert, no exception |
0 / b (b != 0) | Returns 0 | Safe; mathematically correct |
0 / 0 | Returns 0 | Silently wrong; should be undefined |
1 / 2 | Returns 0 | Truncation; any small/large division yields 0 |
999999 / 1000000 | Returns 0 | Near-100% precision loss for ratios < 1 |
MAX_UINT256 / 1 | Returns MAX_UINT256 | Safe; identity division |
MAX_UINT256 / MAX_UINT256 | Returns 1 | Safe; identity |
MAX_UINT256 / 2 | Returns (2^256 - 1) / 2 = 2^255 - 1 | Truncates the 0.5 remainder |
small / large (small < large) | Returns 0 | Always 0; dangerous in share/price calculations |
(a * b) / c vs a * (b / c) | Different results | Operation ordering changes output due to intermediate truncation |
Real-World Exploits
Exploit 1: Balancer V2 — $128M Drained via Rounding Error (November 2025)
Root cause: Accumulated rounding-down errors in token decimal scaling, compounded through batch swap operations.
Details: On November 3, 2025, attackers drained $128.64 million from Balancer V2’s ComposableStablePool contracts across six blockchain networks in under 30 minutes. The vulnerability lay in the _upscaleArray function, which normalizes tokens of different decimal precisions (e.g., 6-decimal USDC to 18-decimal internal precision). This function always rounded down via integer division.
When token balances reached specific rounding boundaries (8-9 wei range), Solidity’s integer division truncated values, creating a systematic precision loss. The attacker compounded this through 65+ micro-swaps executed within a single constructor call, using batchSwap operations that stacked microscopic undercharges. Each individual rounding error was negligible, but the cumulative effect artificially deflated the pool’s invariant (D), which directly determines BPT price via BPT price = D / totalSupply.
The attacker bought underpriced BPT and redeemed it for full-value underlying tokens. Balancer’s centralized Vault architecture — where a single contract holds all pool tokens — meant one vulnerability in pool math impacted every ComposableStablePool simultaneously.
Ironically, a developer comment in the code read: “the impact of this rounding is expected to be minimal.” It was off by $128 million.
DIV’s role: Every _upscale and invariant calculation used integer division that truncated toward zero. The attacker’s batch swaps were specifically structured to maximize the cumulative truncation error across many sequential divisions.
References:
- Check Point Research: How an Attacker Drained $128M from Balancer
- OpenZeppelin: Understanding the Balancer V2 Exploit
Exploit 2: ResupplyFi — $9.56M via Exchange Rate Collapse to Zero (June 2025)
Root cause: Integer division producing zero in an exchange rate calculation, bypassing solvency checks entirely.
Details: On June 25, 2025, an attacker exploited ResupplyFi’s lending protocol hours after a new ResupplyPair contract was deployed. The attack combined a classic ERC4626 first-deposit manipulation with a division-by-zero consequence:
- Attacker deposited 1 wei of collateral and minted a single vault share
- Attacker donated 2,000 crvUSD directly to the vault, inflating the share price to ~2 * 10
- ResupplyFi’s exchange rate formula computed
1e36 / oraclePrice. With the manipulated price at ~2e36, integer division produced:1e36 / 2e36 = 0 - With an exchange rate of 0, the loan-to-value (LTV) calculation also evaluated to 0
- An LTV of 0 meant the attacker appeared infinitely solvent
- The attacker borrowed the full $10 million reUSD limit using 1 wei of collateral
The attack used flash loans from Morpho and Curve swaps to fund the initial donation. Stolen funds were distributed across multiple addresses and laundered through Tornado Cash.
DIV’s role: The critical moment was 1e36 / 2e36 = 0 — integer division truncating a sub-1 result to zero. This zero propagated through the solvency check, turning it into a no-op. The EVM’s DIV didn’t revert because the denominator wasn’t zero; it was just larger than the numerator, producing a legitimate but devastating zero result.
References:
- SolidityScan: ResupplyFi Hack Analysis
- Halborn: Explained — The Resupply Hack
- Blockscope: ResupplyFi Exploit — From 1 Wei to $10M
Exploit 3: Yearn Finance yETH — $9M via Rounding-Down in Invariant Solver (November 2025)
Root cause: Unsafe arithmetic in the _calc_supply() invariant solver where rounding-down errors in division zeroed out critical product terms, enabling supply manipulation.
Details: On November 30, 2025, an attacker exploited the yETH Weighted Stable Pool. The pool’s bonding curve calculations used division operations without adequate precision guards. In the invariant solver’s iterative computation, rounding-down errors from integer division caused internal product terms to reach zero. Once a product term was zeroed, subsequent calculations diverged from the intended curve, allowing the attacker to mint far more yETH than their deposit warranted.
The attack had two phases:
-
Primary exploit (~$8.1M): Flash-loaned funds were used to create extreme pool ratios where the invariant solver’s division-based rounding errors were maximized. The zeroed product terms allowed the attacker to drain wstETH, rETH, and cbETH from the pool.
-
Secondary exploit (~$0.9M): After draining the pool to zero supply, a non-disabled bootstrap path reactivated. The attacker deposited just 16 wei across eight tokens, and the protocol — using cached virtual balances that weren’t reset — minted approximately 2.35 * 10^56 yETH tokens, which were swapped for real assets.
DIV’s role: The invariant solver’s iterative formula performed divisions at each step. When pool ratios were extreme, these divisions truncated intermediate values to zero, breaking the mathematical invariant the pool relied on for correct pricing.
References:
Exploit 4: Wise Lending — $466K via ERC4626 Vault Inflation (January 2024)
Root cause: Exchange rate inflation through donation, combined with withdrawal rounding, enabling iterative rate manipulation.
Details: An attacker exploited Wise Lending’s ERC4626 vault implementation through a sophisticated donation attack:
- Deposited a minimal amount to receive initial shares
- Donated assets worth 5,000,000x the initial deposit directly to the vault, inflating the exchange rate
- Wise Lending had a “throttle” mechanism intended to prevent large donations, but the attacker stayed just below the limit
- On withdrawal, the vault’s
assets = shares * totalAssets / totalSupplycalculation rounded down, creating unaccounted-for tokens that effectively acted as additional donations - By iterating deposit-donate-withdraw cycles, the attacker ratcheted the exchange rate from 1:1 to 4:1
- Used the artificially valued vault shares as collateral to borrow $466K from other pools (USDC, WETH, USDT, DAI), then burned the inflated shares
DIV’s role: The share calculation assets * totalSupply / totalAssets truncated via DIV at each step. The truncation error on withdrawal created orphaned assets that increased the rate further, compounding across iterations.
References:
- SolidityScan: Wise Lending Hack Analysis
- Euler Finance: Exchange Rate Manipulation in ERC4626 Vaults
Exploit 5: Compound V2 First-Deposit Bug — Systemic Vulnerability Across Forks
Root cause: Integer division in cToken minting formula rounds to zero when the exchange rate is artificially inflated.
Details: This is a vulnerability class rather than a single incident, affecting Compound V2 and dozens of its forks (Cream Finance, Rari Capital, Hundred Finance, etc.). The core issue:
The cToken exchange rate is calculated as exchangeRate = underlyingBalance * 1e18 / totalSupply. The minted cTokens for a deposit are mintAmount = depositAmount / exchangeRate. When an attacker:
- Mints the smallest possible cToken amount on a fresh market (
totalSupply == 0) - Transfers underlying tokens directly to the cToken contract (bypassing
mint()) - The
exchangeRatebecomes enormous:(deposit + donation) * 1e18 / 1 - A victim’s
mintAmount = victimDeposit / enormousRatetruncates to 0 via DIV - Victim’s tokens are absorbed by the contract; attacker redeems their shares for everything
This pattern was reported across multiple audit contests (Sherlock, Code4rena, Immunefi) and has been independently discovered and exploited on numerous Compound forks. The fix involves “dead shares” (minting an initial non-redeemable amount) or using virtual assets/shares as in OpenZeppelin’s ERC4626 implementation.
DIV’s role: The mintAmount = depositAmount / exchangeRate calculation is a direct application of the DIV opcode. The truncation to zero is not a bug in DIV — it’s working exactly as specified — but the consequence of truncation in a financial context is catastrophic.
References:
- Akshay Srivastav: First Deposit Bug in CompoundV2
- DeFi Hack Labs: Solidity Security — First Deposit Bug
Attack Scenarios
Scenario A: Vault Inflation / Donation Attack (Share Price Manipulation)
contract VulnerableVault {
uint256 public totalShares;
uint256 public totalAssets;
function deposit(uint256 assets) external returns (uint256 shares) {
if (totalShares == 0) {
shares = assets;
} else {
shares = assets * totalShares / totalAssets; // DIV truncates here
}
totalShares += shares;
totalAssets += assets;
// ... transfer assets from sender
}
function withdraw(uint256 shares) external returns (uint256 assets) {
assets = shares * totalAssets / totalShares;
totalShares -= shares;
totalAssets -= assets;
// ... transfer assets to sender
}
}
// Attack:
// 1. attacker.deposit(1) -> gets 1 share, totalAssets=1, totalShares=1
// 2. attacker transfers 1e18 tokens directly to vault contract
// now totalAssets=1e18+1, totalShares=1
// 3. victim.deposit(5e17) -> shares = 5e17 * 1 / (1e18+1) = 0 (truncated!)
// victim receives 0 shares, but 5e17 was added to totalAssets
// 4. attacker.withdraw(1) -> gets (1e18+1+5e17) * 1 / 1 = 1.5e18+1
// attacker profits 5e17 (the victim's entire deposit)Scenario B: Division-by-Zero Solvency Bypass
contract VulnerableLending {
function borrow(uint256 amount, uint256 collateralTokenId) external {
uint256 collateralValue = oracle.getPrice(collateralTokenId);
// If oracle returns 0 (manipulated or stale), ltv = amount * 1e18 / 0
// EVM DIV returns 0, NOT revert
// With unchecked or assembly: ltv = 0, passes any maxLTV check
uint256 ltv;
assembly {
ltv := div(mul(amount, 0xDE0B6B3A7640000), collateralValue) // 1e18
}
require(ltv <= MAX_LTV, "Undercollateralized");
// ltv == 0 always passes! Attacker borrows unlimited with zero collateral value
_executeBorrow(msg.sender, amount);
}
}Scenario C: Dust Extraction via Repeated Rounding
contract VulnerableStaking {
uint256 public totalStaked;
uint256 public totalRewards;
function claimReward(uint256 staked) external returns (uint256 reward) {
// Rounds UP: gives attacker 1 wei extra per claim
reward = (staked * totalRewards + totalStaked - 1) / totalStaked;
totalRewards -= reward;
// ... transfer reward
}
}
// Attack on L2 (gas is cheap):
// Attacker stakes 1 wei, calls claimReward(1) thousands of times.
// Each call extracts ceil(totalRewards / totalStaked) instead of floor.
// Over many iterations, the attacker extracts significantly more than their
// proportional share. On L2 with ~$0.001 per tx, this is profitable.Scenario D: Multiply-Before-Divide vs. Divide-First Precision Attack
// AMM fee calculation -- two implementations with different results
contract AMM {
uint256 constant FEE_BPS = 30; // 0.3%
uint256 constant BPS = 10000;
// VULNERABLE: divides first, loses precision
function getAmountOutBad(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
external pure returns (uint256)
{
uint256 amountInAfterFee = amountIn - (amountIn / BPS) * FEE_BPS;
// amountIn / BPS truncates first; for amountIn=33, result is 0 fee
return reserveOut * amountInAfterFee / (reserveIn + amountInAfterFee);
}
// SAFE: multiplies first, preserves precision
function getAmountOutGood(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
external pure returns (uint256)
{
uint256 amountInAfterFee = amountIn * (BPS - FEE_BPS) / BPS;
return reserveOut * amountInAfterFee / (reserveIn + amountInAfterFee);
}
}
// For amountIn=33: Bad returns MORE tokens (lower effective fee)
// Attacker splits large trade into many 33-token trades to avoid feesMitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Division by zero | Use Solidity >= 0.8.0 (automatic revert on zero denominator) | Default behavior; compiler inserts ISZERO check before DIV |
| T1: Assembly/unchecked | Explicit zero-check before every division in raw code | require(denominator != 0) or if (denominator == 0) revert() |
| T2: Vault inflation | Use virtual assets and virtual shares to prevent first-depositor manipulation | OpenZeppelin ERC4626: _decimalsOffset() adds virtual shares/assets |
| T2: Vault inflation | Mint “dead shares” on vault initialization | First mint sends minimum shares to address(0) or address(1) |
| T3: Operation ordering | Always multiply before dividing | (a * b) / c not (a / c) * b; use mulDiv for overflow safety |
| T3: Overflow in mul-first | Use 512-bit intermediate precision | Uniswap V3 FullMath.mulDiv() or OpenZeppelin Math.mulDiv() |
| T4: AMM rounding | Round against the user / in protocol’s favor | Use mulDivUp for withdrawals; mulDivDown for deposits |
| T5: Dust extraction | Enforce minimum operation sizes; rate-limit claims | require(amount >= MIN_AMOUNT) or per-block claim limits |
| T5: L2 dust attacks | Add gas-equivalent cost floor for repeated operations | Minimum fee per operation that exceeds potential dust profit |
| T6: Solvency bypass | Validate oracle outputs are non-zero and within sanity bounds | require(price > 0 && price < MAX_REASONABLE_PRICE) |
| General | Round in the protocol’s favor on every user-facing operation | Deposits: round shares down; Withdrawals: round assets down; Fees: round up |
Compiler/EIP-Based Protections
- Solidity 0.8.0+ (2020): Automatic revert on division by zero for all
/and%operations. This eliminated the silent-zero-return class of bugs at the language level. - OpenZeppelin Math library: Provides
mulDiv(a, b, denominator)andmulDiv(a, b, denominator, Rounding.Ceil)for precise division with controlled rounding direction and overflow-safe 512-bit intermediates. - Uniswap V3 FullMath:
mulDiv()andmulDivRoundingUp()for 256-bit multiplication with 512-bit intermediate, then division — the gold standard for DeFi arithmetic. - OpenZeppelin ERC4626: Uses a configurable
_decimalsOffset()to add virtual shares and assets, making inflation attacks economically infeasible by requiring donations proportional to 10^(decimals + offset).
Safe Rounding Direction Reference
The correct rounding direction is a security policy decision, not a mathematical preference:
| Operation | Round Direction | Rationale |
|---|---|---|
| Mint shares on deposit | Down (floor) | User receives fewer shares; prevents free share dust |
| Calculate assets on withdrawal | Down (floor) | User receives fewer assets; prevents draining |
| Protocol fee accrual | Up (ceil) | Protocol receives at least the minimum fee |
| Liquidation threshold check | Up (ceil) | More aggressive liquidation protects solvency |
| Interest owed by borrower | Up (ceil) | Borrower always pays at least the minimum |
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | Medium | ResupplyFi ($9.56M), Reserve EasyAuction DoS |
| T2 | Smart Contract | Critical | High | Balancer (466K), Compound V2 forks |
| T3 | Smart Contract | High | High | Balancer V2 _upscale, Yearn yETH ($9M) |
| T4 | Smart Contract | High | Medium | Balancer ($128M via batch swap compounding) |
| T5 | Smart Contract | Medium | Medium (L2) / Low (L1) | Various audit findings; economically viable on L2 |
| T6 | Smart Contract | High | Medium | ResupplyFi ($9.56M solvency bypass) |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
| P3 | Protocol | Medium | Low | Behavioral differences across compiler versions |
Related Opcodes
| Opcode | Relationship |
|---|---|
| SDIV (0x05) | Signed division; same truncation and division-by-zero behavior, but rounds toward zero (not floor) for negative values. Two’s complement edge case: type(int256).min / -1 overflows. |
| MOD (0x06) | Unsigned modulus; returns the remainder that DIV discards. a == (a / b) * b + (a % b). Also returns 0 on modulus-by-zero. |
| MUL (0x02) | Multiplication; the critical counterpart to DIV. Multiply-before-divide preserves precision but risks overflow. mulDiv libraries solve both. |
| MULMOD (0x09) | (a * b) % N with 512-bit intermediate; avoids overflow in multiply-then-modulus. Used in precision-sensitive DeFi math. |
| ADDMOD (0x08) | (a + b) % N with overflow-safe intermediate; used alongside MULMOD for modular arithmetic. |
| ISZERO (0x15) | Used in the Solidity 0.8.0+ division-by-zero check: ISZERO(denominator) + JUMPI to revert before DIV executes. |
| LT (0x10) | Used to detect when small / large == 0 would cause unacceptable precision loss; guards against truncation-to-zero in share calculations. |