Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x02 |
| Mnemonic | MUL |
| Gas | 5 |
| Stack Input | a, b |
| Stack Output | (a * b) % 2^256 |
| Behavior | Unsigned 256-bit multiplication. Result wraps modulo 2^256 on overflow. No overflow flag or exception. |
Threat Surface
MUL is the most dangerous basic arithmetic opcode in the EVM. While ADD overflows only when the sum of two operands exceeds 2^256, MUL overflows when the product of two operands exceeds 2^256 — which happens far more easily. Two 129-bit numbers (each fitting comfortably in a uint256) multiply to a 258-bit result that silently wraps. Even two 128-bit numbers can overflow: (2^128) * (2^128) = 2^256, which wraps to exactly 0.
This asymmetric explosion in magnitude makes MUL the opcode most likely to produce catastrophically wrong results from seemingly reasonable inputs. A token balance of 10^18 (1 ETH in wei) multiplied by a price of 10^18 produces 10^36 — still within uint256 range. But multiply three such values together (common in DeFi fee/rate calculations) and 10^54 is still safe, yet the margins narrow rapidly. Four-way products or values near 10^38 easily overflow.
The EVM provides no overflow flag, no widening multiplication (no 512-bit result), and no trap. The caller receives the lower 256 bits of the true product with no indication that information was lost. Combined with pre-Solidity 0.8.0’s lack of overflow checks, this made MUL the root cause of the single most iconic smart contract exploit: the BEC batchOverflow attack.
Smart Contract Threats
T1: Multiplication Overflow / Silent Wrapping (Critical)
The EVM’s MUL wraps silently: (2^128) * (2^128) == 0. Any contract that multiplies user-controlled values without overflow checking is vulnerable. MUL overflows far more easily than ADD because magnitude grows quadratically:
- Two 128-bit numbers overflow:
2^128 * 2^128 = 2^256 ≡ 0 (mod 2^256) - Two 129-bit numbers: guaranteed overflow with information loss
- Contrast with ADD: two 255-bit numbers added still fit in 256 bits; two 128-bit numbers multiplied do not
Common vulnerable patterns:
- Batch operations:
uint256 amount = cnt * _valuewhere both are user-controlled (the BEC exploit) - Price calculations:
tokenAmount * pricePerTokenwhere attacker manipulates price via oracle or flash loan - Fee computation:
amount * feeRate / FEE_DENOMINATORwhereamount * feeRateoverflows before the division - Interest accrual:
principal * (1 + rate)^periodscomputed iteratively with unchecked multiplication
Pre-Solidity 0.8.0: No automatic overflow check. SafeMath was required but frequently omitted for MUL.
Solidity 0.8.0+: Automatic revert on overflow. The compiler inserts a division-based check: require(a == 0 || a * b / a == b).
T2: Phantom Zero Wrapping (Critical)
A particularly insidious MUL behavior: carefully chosen operands can make the product wrap to exactly 0. When a * b ≡ 0 (mod 2^256) despite both a != 0 and b != 0, downstream code that checks require(amount > 0) or require(balance >= amount) where amount is the overflowed product will pass trivially.
This is the exact mechanism behind the BEC batchOverflow exploit: 2 * 0x8000...0000 = 2^256 ≡ 0. The zero product bypassed balance checks, enabling unlimited token minting.
Phantom-zero wrapping occurs whenever one operand is a multiple of 2^k and the other contributes enough factors of 2 to push the total factor past 256. More generally, it occurs whenever the true product is an exact multiple of 2^256.
T3: DeFi Price and Rate Calculations (High)
DeFi protocols rely heavily on MUL for price calculations, fee assessments, and liquidity math. Common patterns where MUL overflow causes financial loss:
- AMM pricing:
reserveOut * amountIn / (reserveIn + amountIn)— the numerator can overflow - Lending rates:
borrowAmount * interestRate * timeElapsed— triple multiplication compounds overflow risk - LP token valuation:
totalAssets * lpTokenAmount / totalLPSupply— numerator overflow destroys precision - Reward distribution:
rewardRate * timeElapsed * stakedAmount— three-factor products
The danger amplifies because DeFi values are typically denominated in wei (10^18), so a seemingly modest calculation like 1000 ETH * price_in_wei * fee_rate involves multiplying values around 10^21 * 10^18 * 10^16 = 10^55, consuming significant uint256 headroom.
T4: Flash Loan Amplification of MUL Overflow (High)
Flash loans allow attackers to temporarily hold enormous token balances (10^24 or more in wei). These inflated values, when multiplied in DeFi calculations, can trigger overflows that would be unreachable with normal balances:
- Borrow 10^24 tokens via flash loan
- Pass them into a function that computes
amount * ratewhererateis 10 10^24 * 10^18 = 10^42, still within uint256, but closer to the boundary- Chain multiple such operations, or target contracts with intermediate multiplications, to trigger overflow
Flash loans convert theoretical MUL overflow risks into practical exploits by removing the economic barrier of acquiring large token positions.
T5: Unchecked Blocks in Solidity 0.8+ (High)
Solidity >= 0.8.0 provides unchecked { } blocks that disable overflow checks. Developers use them for gas optimization in “known-safe” math. MUL inside unchecked blocks is especially dangerous because:
- Overflow magnitudes are much larger than ADD (quadratic vs. linear)
- Developer intuition about “safe” ranges is often wrong for multiplication
- A value that’s safe to add 1000 times may not be safe to multiply even twice
unchecked {
uint256 result = a * b; // Developer assumes a < 10^18 and b < 10^18
// But after a protocol upgrade, 'a' can now be up to 10^30...
}Gas-optimized DeFi protocols (Uniswap v3/v4, Balancer, Curve) use unchecked multiplication extensively in their core math libraries. These are audited for safety, but forks and modifications frequently introduce errors.
T6: Multiplication Constant Mismatches (High)
When contracts use multiplication with magic constants (fee rates, scaling factors, precision multipliers), a mismatch between the constant used in the check and the constant used in the calculation creates exploitable discrepancies. The Uranium Finance exploit ($57M) was caused by exactly this pattern: the swap function used 100 as a scaling factor while the invariant check used 1000, creating a 100x discrepancy that attackers drained.
// Dangerous: inconsistent constants
uint256 adjustedBalance = balance * 100; // Scaling by 100
require(adjustedBalance0 * adjustedBalance1 >= reserve0 * reserve1 * 1000 * 1000); // Checking against 1000
// 100^2 vs 1000^2 = 10,000 vs 1,000,000 -- 100x gapProtocol-Level Threats
P1: No DoS Vector (Low)
MUL costs a fixed 5 gas regardless of operand size. It cannot be used for gas griefing. Unlike EXP (which has variable gas cost based on the exponent), MUL’s gas is constant and trivial. It operates purely on the stack with no memory or storage access.
P2: Consensus Safety (Low)
MUL is deterministic: (a * b) mod 2^256 is unambiguous for any pair of 256-bit unsigned integers. All EVM client implementations (geth, Nethermind, Besu, Erigon, reth) agree on its behavior. No known consensus divergence has occurred due to MUL.
The underlying big-integer multiplication is a well-understood operation, and the modular reduction is trivial. The only theoretical risk would be an implementation using a non-standard multiplication algorithm that produces incorrect truncation, but this has never been observed.
P3: No Direct State Impact (None)
MUL modifies only the stack. It cannot cause state bloat, storage writes, or memory expansion.
P4: Compiler-Generated Overflow Checks (Low)
Solidity 0.8+ inserts overflow detection after MUL using a DIV-based pattern:
// Compiler-generated check for a * b
PUSH a
PUSH b
MUL // result = (a * b) mod 2^256
DUP1 // result, result
PUSH a
DUP1 // a, a, result, result
ISZERO // a == 0?, a, result, result
SWAP1 // a, a == 0?, result, result
DIV // result / a, a == 0?, result
SWAP1 // a == 0?, result / a, result
OR // (a == 0? || result / a), result
PUSH b
EQ // (a == 0 || result / a == b)?
This adds ~20 gas overhead per MUL. Different compiler versions may emit slightly different check patterns. Auditors reviewing bytecode should be aware that the same Solidity source produces different opcodes depending on compiler version — a solc 0.7.x contract has raw MUL with no checks while solc 0.8.x wraps it in the verification pattern above.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
MAX_UINT256 * 2 | Returns MAX_UINT256 - 1 | Near-max wrapping; result looks plausible but is wildly incorrect |
MAX_UINT256 * MAX_UINT256 | Returns 1 | Two maximum values produce 1 — extreme information loss |
0 * anything | Returns 0 | Safe; identity. Used in overflow check patterns (a == 0 short-circuit) |
1 * anything | Returns anything | Identity; no issue |
2^128 * 2^128 | Returns 0 | Phantom zero: two “reasonable” 128-bit values wrap to exactly zero |
2^128 * (2^128 + 1) | Returns 2^128 | Wraps to a small value; product of two large numbers yields a small one |
(2^256 - 1) * (2^256 - 1) | Returns 1 | (-1) * (-1) = 1 in two’s complement; correct for signed, deceptive for unsigned |
a * 2^n (power of 2) | Equivalent to SHL(n, a) | Compilers optimize this; but SHL also wraps silently. No safety difference |
a * 10^18 (wei scaling) | Overflows when a > 2^256 / 10^18 ≈ 1.15 * 10^59 | Limits practical token amounts; relevant for high-supply tokens |
| Odd * Even | Low bits of result are zero | Can mask overflow detection if checking only low-order bits |
Real-World Exploits
Exploit 1: BeautyChain (BEC) “batchOverflow” — Infinite Token Minting (April 2018)
CVE: CVE-2018-10299
Root cause: Integer overflow in MUL operation within the batchTransfer() function. The expression uint256(cnt) * _value overflowed to zero, bypassing all balance checks.
Details: The batchTransfer function was designed to send equal amounts of BEC tokens to multiple recipients. It computed the total amount as uint256 amount = uint256(cnt) * _value, where cnt was the number of recipients and _value was the per-recipient amount. Both values were user-controlled via the function parameters.
The attacker passed two recipients (cnt = 2) and _value = 0x8000000000000000000000000000000000000000000000000000000000000000 (2^255). The MUL operation computed 2 * 2^255 = 2^256, which wraps to exactly 0 modulo 2^256. This phantom-zero result then:
- Passed
require(balances[msg.sender] >= amount)sincebalance >= 0is always true - Subtracted 0 from the sender’s balance (no cost)
- Added 2^255 to each of two recipients’ balances via ADD in a loop
function batchTransfer(address[] _receivers, uint256 _value) public returns (bool) {
uint cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value; // 2 * 2^255 = 0 (overflow)
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount); // 0 >= 0 passes
balances[msg.sender] = balances[msg.sender].sub(amount); // Subtracts 0
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value); // Adds 2^255
}
return true;
}MUL’s role: MUL was the direct cause. The overflow happened in the MUL operation itself (cnt * _value), producing the phantom zero that nullified all downstream safety checks. This is the canonical example of MUL overflow exploitation.
Impact: The attacker received ~1.16 * 10^59 BEC tokens (effectively infinite supply). OKEx suspended all ERC-20 token deposits and withdrawals. PeckShield identified over a dozen other ERC-20 contracts with identical vulnerabilities. The exploit triggered industry-wide adoption of SafeMath.
References:
- PeckShield: batchOverflow Bug in Multiple ERC20 Contracts
- SECBIT: BEC Vulnerability Analysis
- CVE-2018-10299
Exploit 2: Uranium Finance — Multiplication Constant Mismatch (April 2021)
Root cause: Inconsistent multiplication constants in the swap function vs. the invariant check of a Uniswap v2 fork. The swap used 100 as its fee scaling factor while the constant-product check used 1000, creating a 100x exploitable discrepancy.
Details: Uranium Finance forked Uniswap v2 and modified the fee structure. In the modified pair.sol, the balance adjustment after a swap multiplied by 100 (i.e., balance * 100), but the K-value invariant check still compared against 1000 * 1000 = 1,000,000 instead of the correct 100 * 100 = 10,000. This meant the check was 100x too lenient:
// Uranium's buggy code (simplified)
uint balance0Adjusted = balance0.mul(100).sub(amount0In); // Scales by 100
uint balance1Adjusted = balance1.mul(100).sub(amount1In); // Scales by 100
require(
balance0Adjusted.mul(balance1Adjusted) >=
_reserve0.mul(_reserve1).mul(1000**2) // But checks against 1000^2 = 1,000,000
);
// 100^2 = 10,000 vs 1000^2 = 1,000,000 -- attacker has 100x slackAn attacker could send 1 wei of input and extract ~98% of the output reserve, because the 100x gap in the invariant check made the extraction appear “balanced.”
MUL’s role: The exploit was fundamentally a MUL-based error — the wrong multiplication constant was applied, and the mismatch between two MUL operations (one with 100, one with 1000) created the exploitable gap. The MUL operations themselves didn’t overflow, but their incorrect usage destroyed the invariant that protects AMM liquidity.
Impact: 18M), 17.9M BUSD, 1,800 ETH (4.3M). US authorities later seized $31M of the stolen funds.
References:
Exploit 3: Truebit Protocol — $26.6M Stolen via Price Calculation Overflow (January 2026)
Root cause: Integer overflow in getPurchasePrice() where intermediate multiplication of large values wrapped to zero, returning a price of 0 ETH for non-zero token amounts.
Details: The Truebit token purchase contract used a bonding curve with polynomial pricing. The getPurchasePrice() function computed intermediate values via multiple chained multiplications:
v12 = 100 * amount^2 * reserve— three-factor MUL chainv9 = 200 * totalSupply * amount * reserve— four-factor MUL chain- Final price:
(v12 + v9) / denominator
The contract was compiled with Solidity 0.6.10 (no automatic overflow checks). By supplying an extremely large amount parameter, the attacker caused both v12 and v9 to overflow, wrapping the entire price calculation to 0. The attacker then:
- Called
buy()withmsg.value = 0, receiving ~240 trillion TRU tokens for free - Sold them back to the contract’s reserve for actual ETH
- Repeated the mint-and-burn cycle 4-5 times in a single transaction
- Drained 8,535 ETH (~$26.6M)
MUL’s role: Multiple chained MUL operations overflowed. The three- and four-factor products (100 * amount * amount * reserve) are especially dangerous because overflow risk grows super-linearly with the number of multiplied terms. This is a textbook example of why multi-term multiplication must be checked at every intermediate step.
Impact: 8,535 ETH (~0.16 to near zero. Funds routed through Tornado Cash. The contract had been deployed in 2021, never audited, and was essentially abandoned.
References:
- Olympix: Truebit Exploit Analysis
- BlockSec: In-Depth Analysis of the Truebit Incident
- ExVul: Truebit Protocol Attack Analysis
Exploit 4: PoWHC (Proof of Weak Hands Coin) — 866 ETH Stolen (January 2018)
Root cause: Integer underflow in balance tracking caused by an exploitable interaction between token transfers and the sell mechanism, producing an astronomically large balance via wrapping arithmetic.
Details: PoWH Coin was an experimental Ponzi-scheme token on Ethereum. The transferFrom() function allowed an approved spender to move tokens between accounts, but the balance deduction was applied to the wrong account in certain edge cases. When the spender sold tokens belonging to another account, the sold amount was subtracted from the spender’s (potentially zero) balance, causing an underflow that wrapped to ~2^256. With this enormous balance, the attacker claimed virtually all dividends held in the contract.
MUL’s role: The dividend distribution calculated each holder’s share as balance * dividendPerToken. With the attacker’s balance at near-2^256, this multiplication — even with a tiny dividendPerToken — produced a value large enough to drain the entire dividend pool. The attacker’s phantom balance, amplified by MUL, converted an underflow into total fund extraction.
Impact: 866 ETH (~$800K at the time) drained from the contract. All depositors lost their funds. The incident became an early cautionary tale about unchecked arithmetic in smart contracts.
References:
Attack Scenarios
Scenario A: Classic Batch Overflow (MUL to Zero)
// Solidity < 0.8.0, no SafeMath on MUL
contract VulnerableAirdrop {
mapping(address => uint256) public balances;
function batchSend(address[] calldata recipients, uint256 valueEach) external {
uint256 total = recipients.length * valueEach; // MUL overflows to 0
require(balances[msg.sender] >= total); // 0 >= 0 passes
balances[msg.sender] -= total; // Subtracts 0
for (uint i = 0; i < recipients.length; i++) {
balances[recipients[i]] += valueEach; // Adds huge value
}
}
}Attack: Pass 2 recipients with valueEach = 2^255. Total overflows to 0. Sender pays nothing; each recipient receives 2^255 tokens.
Scenario B: Price Calculation Overflow in DeFi
// Solidity < 0.8.0
contract VulnerableLending {
uint256 constant PRECISION = 1e18;
function calculateInterest(
uint256 principal,
uint256 ratePerSecond,
uint256 elapsed
) public pure returns (uint256) {
// Three-factor multiplication: overflow risk grows with each term
return principal * ratePerSecond * elapsed / PRECISION;
// If principal=10^24, rate=10^16, elapsed=10^7:
// 10^24 * 10^16 = 10^40 (still fits)
// 10^40 * 10^7 = 10^47 (still fits, but margins narrow)
// Flash loan with principal=10^30 pushes it over
}
}Attack: Flash-borrow a massive amount to make principal large enough that the triple product overflows, wrapping interest to a small value. Borrow at near-zero effective interest, profit on arbitrage.
Scenario C: Phantom Zero in Token Sale
// Solidity < 0.8.0
contract VulnerableSale {
IERC20 public token;
uint256 public pricePerToken; // in wei
function buy(uint256 tokenAmount) external payable {
uint256 cost = tokenAmount * pricePerToken; // Overflows to 0
require(msg.value >= cost); // 0 >= 0 passes with 0 ETH
token.transfer(msg.sender, tokenAmount);
}
}Attack: Choose tokenAmount such that tokenAmount * pricePerToken wraps to 0. Send 0 ETH, receive tokens for free. For example, if pricePerToken = 2, set tokenAmount = 2^255.
Scenario D: Unchecked MUL in Gas-Optimized Library
// Solidity 0.8.x -- unchecked block reintroduces vulnerability
library FastMath {
function mulWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
unchecked {
z = x * y; // No overflow check!
z = z / 1e18; // Division by WAD
// If x * y overflowed, z is now a small garbage value
// Developer assumed x and y are both < 2^128, but didn't enforce it
}
}
}
contract VulnerableVault {
using FastMath for uint256;
function withdrawShares(uint256 shares) external {
uint256 assets = shares.mulWad(pricePerShare); // Overflows in unchecked
// assets is now tiny; user withdraws less than they should
// OR: attacker deposits with overflowed amount, gets more shares than deserved
_burn(msg.sender, shares);
asset.transfer(msg.sender, assets);
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Multiplication overflow | Use Solidity >= 0.8.0 (automatic overflow revert) | Default behavior; compiler inserts a == 0 || a * b / a == b check |
| T1: Legacy contracts | Use OpenZeppelin SafeMath library | using SafeMath for uint256; a.mul(b) |
| T2: Phantom zero wrapping | Validate inputs before multiplication | require(_value <= MAX_SAFE_VALUE) or use SafeMath |
| T3: DeFi price overflow | Use mulDiv for (a * b) / c patterns | OpenZeppelin Math.mulDiv() or Solmate FullMath.mulDiv() computes (a * b) / c without intermediate overflow using 512-bit math |
| T4: Flash loan amplification | Cap input amounts; validate intermediate results | require(amount <= MAX_AMOUNT) before any multiplication |
| T5: Unchecked blocks | Minimize unchecked MUL; enforce input bounds | Restrict unchecked to provably safe ranges; add require(a <= type(uint128).max) before unchecked MUL |
| T6: Constant mismatches | Use named constants; test invariants | Define FEE_FACTOR = 100 once and reference everywhere; fuzz-test the invariant |
| General: Multi-term products | Check overflow at each intermediate step | temp = a * b; result = temp * c; with checked arithmetic on each step |
| General: Static analysis | Use automated overflow detection tools | Slither (slither --detect unchecked-mul), Mythril, Certora formal verification |
Compiler/EIP-Based Protections
- Solidity 0.8.0+ (2020): Automatic revert on MUL overflow. The compiler uses a division-based check: if
a != 0, verify(a * b) / a == b. This eliminated the most common MUL overflow patterns. - SafeMath (OpenZeppelin): Pre-0.8.0 library that wraps MUL with the same division-based overflow check. The
mulfunction became the standard from 2017-2020. - mulDiv libraries: OpenZeppelin’s
Math.mulDiv()and Uniswap’sFullMath.mulDiv()compute(a * b) / cusing 512-bit intermediate representation, avoiding overflow entirely. Critical for DeFi protocols. - Static analysis tools: Slither, Mythril, and Securify detect unchecked multiplication. Formal verification tools (Certora, Halmos) can prove overflow impossibility for bounded inputs.
Overflow Detection at EVM Level
The standard pattern for detecting MUL overflow at the opcode level:
// Check: did (a * b) overflow?
// If a != 0 and (a * b) / a != b, overflow occurred
PUSH a
PUSH b
MUL // result = (a * b) mod 2^256
PUSH a
DUP1 // a, a
ISZERO // a == 0?
jumpi @safe // If a == 0, no overflow possible
DUP2 // result, a
DIV // result / a
PUSH b
EQ // result / a == b?
jumpi @safe
REVERT // Overflow detected
@safe:
This is equivalent to what Solidity 0.8.0+ emits after every MUL instruction.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | High (pre-0.8) / Low (post-0.8) | BEC batchOverflow (26.6M) |
| T2 | Smart Contract | Critical | Medium (pre-0.8) / Low (post-0.8) | BEC (product wrapped to exactly 0) |
| T3 | Smart Contract | High | Medium | Truebit (chained MUL in pricing), various DeFi |
| T4 | Smart Contract | High | Medium | Flash loan + MUL overflow combinations |
| T5 | Smart Contract | High | Medium | Emerging risk in gas-optimized protocols |
| T6 | Smart Contract | High | Low | Uranium Finance ($57.2M) |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
Related Opcodes
| Opcode | Relationship |
|---|---|
| ADD (0x01) | Addition overflows less easily than MUL; often combined in exploit chains (BEC used MUL overflow + ADD for balance inflation) |
| DIV (0x04) | Used in MUL overflow detection pattern (a * b / a == b); also critical for mulDiv safe patterns |
| MULMOD (0x09) | Modular multiplication avoids overflow by design ((a * b) % N), but introduces modular arithmetic semantics |
| EXP (0x0A) | Exponentiation overflows even faster than MUL; a^n is repeated multiplication with compounding overflow risk |
| SHL (0x1B) | Left shift is equivalent to multiplication by powers of 2 (a * 2^n == a << n); same wrapping behavior, lower gas (3 vs 5) |