Opcode Summary

PropertyValue
Opcode0x09
MnemonicMULMOD
Gas8
Stack Inputa, b, N
Stack Output(a * b) % N
BehaviorModular multiplication with 512-bit intermediate. Computes (a * b) % N where the intermediate product a * b is computed in full 512-bit precision (no overflow). If N == 0, returns 0.

Threat Surface

MULMOD is unique among EVM arithmetic opcodes: it is the only native instruction that performs 512-bit intermediate computation. When MULMOD computes (a * b) % N, the multiplication a * b is calculated in full 512-bit precision before the modular reduction, meaning no information is lost to overflow regardless of the operand magnitudes. This stands in sharp contrast to MUL, which silently truncates to the lower 256 bits.

This property makes MULMOD the foundational building block for two critical domains:

  1. Cryptographic implementations: On-chain elliptic curve arithmetic (outside precompiles), modular exponentiation, and zk-proof verifiers depend on MULMOD for field arithmetic over prime moduli. Incorrect modulus selection, off-by-one errors in field parameters, or accidentally using MUL instead of MULMOD breaks the algebraic structure that cryptographic security relies upon.

  2. DeFi full-precision math: The mulDiv pattern — computing (a * b) / c without intermediate overflow — is implemented using MULMOD as the key step to extract the 512-bit product. Uniswap v3/v4, OpenZeppelin Math.mulDiv(), Solmate FullMath, and PRBMath all use MULMOD internally. Bugs in these libraries propagate to every protocol that depends on them.

The N == 0 case returns 0 at the EVM level (defined in the Yellow Paper), but this seemingly benign behavior caused a consensus-level denial-of-service vulnerability in go-ethereum (GHSA-jm5c-rv3w-w83m) where the underlying uint256 library panicked on modulo-zero, crashing nodes. This demonstrates that even “safe by spec” edge cases can be dangerous in implementation.

Unlike MUL, MULMOD cannot silently produce a wrong answer due to overflow — but it introduces a different class of errors: modular arithmetic semantics that developers accustomed to standard integer math may misunderstand, precision loss when used in fixed-point libraries, and subtle rounding errors that compound across multiple operations.


Smart Contract Threats

T1: Modulus-by-Zero Returning Silent Zero (High)

When N == 0, MULMOD returns 0 regardless of a and b. The EVM does not revert. This “zero-absorbing” behavior can mask errors in contracts that dynamically compute the modulus:

  • If N is derived from user input, storage, or an oracle and happens to be zero, the result silently becomes zero
  • Downstream code that uses the MULMOD result in division, branching, or transfer amounts operates on an incorrect value
  • Unlike DIV-by-zero (which also returns 0), MULMOD-by-zero is more dangerous because it occurs in contexts where the result is expected to be a meaningful mathematical value (a field element, a scaled price)

Solidity’s mulmod(a, b, 0) does not revert in any compiler version. The built-in function maps directly to the opcode with no guard. Developers must add explicit require(N != 0) checks.

function computeFieldElement(uint256 a, uint256 b, uint256 modulus) public pure returns (uint256) {
    // If modulus is 0 (e.g., uninitialized or from a failed oracle read),
    // this returns 0 -- potentially interpreted as "valid field element zero"
    return mulmod(a, b, modulus);
}

T2: Precision Loss in mulDiv-Based Fixed-Point Arithmetic (High)

The dominant use of MULMOD in DeFi is within mulDiv implementations that compute (a * b) / c without intermediate overflow. These libraries use MULMOD to extract the remainder of the 512-bit product:

assembly {
    let mm := mulmod(a, b, not(0))  // remainder of (a*b) / 2^256
    prod0 := mul(a, b)              // lower 256 bits
    prod1 := sub(sub(mm, prod0), lt(mm, prod0))  // upper 256 bits
}

Bugs in this pattern have real consequences:

  • Uniswap v3 mulDivRoundingUp overflow: The original implementation added 1 for rounding without checking for type(uint256).max overflow: mulDiv(a, b, denominator) + (mulmod(a, b, denominator) > 0 ? 1 : 0). When mulDiv returned type(uint256).max and the remainder was non-zero, the addition overflowed. Fixed in commit 3face0c.
  • PRBMath precision bugs (Certora 2023): Certora’s formal verification found that even well-tested, widely-used fixed-point libraries accumulate rounding errors across multiple operations, creating exploitable precision drift in lending protocols and AMMs.

The core danger: MULMOD itself is exact, but the surrounding 512-bit division logic is complex (~50 instructions) and error-prone. A single off-by-one in the modular inverse or subtraction chain corrupts every downstream calculation.

T3: Incorrect Field Arithmetic in Cryptographic Contracts (Critical)

On-chain cryptographic implementations (elliptic curve operations, RSA verification, custom zk-circuits) use MULMOD for modular multiplication over a prime field. Errors in this context break cryptographic guarantees:

  • Wrong modulus: Using p - 1 instead of p, or a composite instead of a prime, for the field modulus destroys the algebraic structure. MULMOD computes the correct modular product regardless — it has no concept of “correct” vs “incorrect” moduli — so the error is purely logical.
  • MUL vs MULMOD confusion: Using MUL (0x02) where MULMOD (0x09) is needed causes silent truncation. For field elements near the modulus (typically 254-256 bits), MUL overflows almost every time, producing values outside the field.
  • Non-reduced inputs: If a >= N or b >= N, MULMOD still produces a correct result (it’s mathematically (a * b) mod N), but this can indicate upstream errors where field elements weren’t properly reduced after addition.
  • Missing final reduction after ADDMOD+MULMOD chains: Complex field expressions involving multiple ADDMOD and MULMOD operations may produce intermediate values that, while correct modulo N, trigger unexpected behavior in comparison or branching logic.

Elliptic curve point validation is especially sensitive: if MULMOD is used incorrectly in the curve equation check y^2 == x^3 + ax + b (mod p), invalid points may be accepted, enabling small-subgroup attacks or invalid-curve attacks.

T4: Assembly-Level MULMOD Misuse (High)

MULMOD is heavily used in inline assembly for gas optimization. Assembly bypasses Solidity’s type system and safety checks, creating several error patterns:

  • Stack ordering errors: MULMOD pops a, b, N in that order. Swapping b and N computes (a * N) % b instead of (a * b) % N — a completely different result that may not cause an obvious failure.
  • Incorrect 512-bit reconstruction: The mulDiv pattern uses MULMOD alongside MUL, SUB, and LT in a specific sequence. Errors in the subtraction-with-borrow step (sub(sub(mm, prod0), lt(mm, prod0))) silently produce a wrong prod1 (upper 256 bits).
  • Memory-safety annotations: OpenZeppelin’s Math.mulDiv() assembly blocks were missing memory-safe annotations (issue #4270), preventing the Solidity optimizer from optimizing surrounding code. While not a security vulnerability per se, it demonstrates how complex assembly MULMOD patterns resist automated verification.
  • Mixing checked and unchecked contexts: Using MULMOD in assembly (always unchecked) alongside Solidity-level checked arithmetic creates inconsistent safety guarantees within the same function.

T5: Incorrect mulDiv for Token Accounting (High)

DeFi protocols use mulDiv (built on MULMOD) for share-to-asset conversions, fee calculations, and price scaling. Incorrect usage patterns:

  • Rounding direction errors: mulDiv rounds down by default. For deposits, rounding down (fewer shares) favors the protocol. For withdrawals, rounding down (fewer assets) also favors the protocol. Reversing this — rounding up on deposits or down on withdrawals from the user’s perspective — creates extractable value. The mulmod(a, b, denominator) > 0 check used for rounding-up variants is exact (because MULMOD is 512-bit precise), but the final +1 can overflow.
  • Denominator-zero: mulDiv(a, b, 0) reverts in well-written libraries (they check require(denominator > 0)), but custom implementations may not. If the denominator is a dynamic value (total supply, reserve balance), an empty pool can trigger division by zero.
  • Result overflow: Even with 512-bit intermediate precision, (a * b) / c can exceed type(uint256).max if c is small relative to a * b. Checked mulDiv implementations verify prod1 < denominator; unchecked ones silently truncate.

T6: Side-Channel via Gas Timing (Low)

MULMOD costs a fixed 8 gas regardless of operand values, so there is no direct gas-based side channel. However, in cryptographic contexts, the choice of whether to execute MULMOD (e.g., in a square-and-multiply exponentiation loop) can leak information about secret exponents through transaction gas consumption patterns. On-chain modular exponentiation should use constant-time patterns or the ModExp precompile (address 0x05).


Protocol-Level Threats

P1: Client Implementation Divergence — The geth MULMOD DoS (Critical, Historical)

GHSA-jm5c-rv3w-w83m / CVE-2020-26265: In go-ethereum versions v1.9.16 through v1.9.17, executing mulmod(a, b, 0) caused a panic in the underlying uint256 library. The root cause was a buffer underflow: when the divisor N == 0, the internal variable dLen remained 0, and the code attempted to access array index [-1], crashing the node.

This vulnerability could be triggered by any transaction containing a contract call that executed MULMOD with N = 0. An attacker could craft such a transaction and broadcast it, crashing all vulnerable geth nodes during block processing and effectively partitioning the network. The issue was discovered when nodes crashed while syncing from genesis on the Ropsten testnet.

The bug was in the uint256 library (fixed in v1.1.1), and geth v1.9.18+ includes the patch. Other EVM clients (Nethermind, Besu, Erigon) were not affected because they used different big-integer implementations. This remains the only known consensus-level vulnerability directly caused by MULMOD.

P2: No DoS Vector from Gas (Low)

MULMOD costs a fixed 8 gas regardless of operand magnitudes. It cannot be used for gas griefing. It operates purely on the stack with no memory or storage access. The internal 512-bit multiplication is more expensive than a 256-bit MUL at the hardware level, but the fixed gas cost means the EVM has already accounted for worst-case execution time.

P3: Consensus Safety Post-Fix (Low)

With the geth uint256 library fix in place, MULMOD is deterministic across all clients: (a * b) mod N with 512-bit intermediate is unambiguous for any triple of 256-bit unsigned integers, and the N == 0 case returns 0 per the Yellow Paper. No consensus divergence has been observed since the geth fix.

P4: No Direct State Impact (None)

MULMOD modifies only the stack. It cannot cause state bloat, storage writes, or memory expansion.


Edge Cases

Edge CaseBehaviorSecurity Implication
N == 0Returns 0Silent zero; crashed geth v1.9.16-v1.9.17. No revert in Solidity. Must guard explicitly.
N == 1Returns 0 (everything mod 1 is 0)Meaningful result is lost; if N is dynamically computed and degenerates to 1, all products become zero.
MAX_UINT256 * MAX_UINT256 mod NCorrect result via 512-bit intermediateUnlike MUL, this does NOT overflow. (2^256-1)^2 mod N is computed exactly. This is MULMOD’s key advantage.
MAX_UINT256 * MAX_UINT256 mod (2^256-1)Returns 1(2^256-1)^2 = 2^512 - 2^257 + 1, and (2^512 - 2^257 + 1) mod (2^256-1) = 1. Correct but non-obvious.
a * b mod N where a < N and b < NStandard field multiplicationThe intended use case for cryptographic field arithmetic. Always safe and exact.
a == 0 or b == 0Returns 0Zero times anything is zero mod anything. Safe identity.
Small N (e.g., N == 2)Returns 0 or 1Valid parity check. No security issue unless N is unexpectedly small due to a bug.
N == 2^256 - 1 (MAX_UINT256)(a * b) mod (2^256-1)Equivalent to multiplication in the Mersenne-like ring. Result fits in 256 bits.
a * b < NReturns a * b (no reduction)Modular reduction is a no-op. Result identical to what MUL would produce (assuming no MUL overflow).
a * b exactly divisible by NReturns 0Legitimate mathematical result, but could surprise developers expecting a nonzero residue.

Real-World Exploits

Exploit 1: go-ethereum MULMOD Denial of Service — Network Partition Risk (August 2020)

CVE: CVE-2020-26265 / GHSA-jm5c-rv3w-w83m

Root cause: Buffer underflow in the uint256 library used by go-ethereum when MULMOD is executed with modulus N == 0.

Details: The EVM specification (Yellow Paper) defines MULMOD(a, b, 0) = 0. However, go-ethereum’s implementation delegated to the holiman/uint256 library, which did not handle the N == 0 case. Internally, the library computed dLen = len(divisor) (which was 0 for a zero divisor), then attempted to access divisor[dLen - 1] — accessing index [-1], triggering a Go runtime panic.

The vulnerability was discoverable by anyone deploying or calling a contract containing:

assembly {
    let result := mulmod(1, 1, 0)  // Crashes geth v1.9.16-v1.9.17
}

A malicious transaction containing this instruction, once included in a block, would crash every geth node processing that block. Since geth represented the overwhelming majority of Ethereum nodes, this could have partitioned the network. The bug was discovered when nodes crashed syncing the Ropsten testnet from genesis (an existing contract on Ropsten happened to execute mulmod(..., 0)).

MULMOD’s role: The vulnerability was directly in MULMOD’s implementation. The opcode’s specification handles N == 0 gracefully (return 0), but the implementation did not, creating a gap between spec and code that enabled a consensus-level DoS.

Impact: All geth nodes running v1.9.16-v1.9.17 were vulnerable to instant crash via a single transaction. The fix was released in geth v1.9.18 with the uint256 library updated to v1.1.1. No mainnet exploitation occurred because the vulnerability was responsibly disclosed and patched before a mainnet trigger.

References:


Exploit 2: Uniswap v3 FullMath.mulDivRoundingUp Overflow (2021)

Root cause: Missing overflow check in the mulDivRoundingUp function where adding 1 for rounding could overflow type(uint256).max.

Details: Uniswap v3’s FullMath library computes (a * b) / denominator using MULMOD to extract the 512-bit intermediate product. The rounding-up variant originally used:

function mulDivRoundingUp(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
    result = mulDiv(a, b, denominator);
    if (mulmod(a, b, denominator) > 0) {
        result++;  // BUG: overflows if result == type(uint256).max
    }
}

When mulDiv returned type(uint256).max and mulmod(a, b, denominator) was nonzero (meaning there was a remainder requiring rounding up), result++ overflowed to 0. This turned a near-maximum value into zero — a catastrophic error in any price or liquidity calculation.

The fix (commit 3face0c) added an explicit overflow guard:

if (mulmod(a, b, denominator) > 0) {
    require(result < type(uint256).max);
    result++;
}

MULMOD’s role: MULMOD was used correctly to determine whether rounding was needed (mulmod(a, b, denominator) > 0 is the exact remainder check). The bug was in the arithmetic surrounding MULMOD — the unguarded result++. MULMOD’s 512-bit precision made the remainder check exact, but the 256-bit result couldn’t hold the rounded-up value in the edge case.

Impact: Potential corruption of tick-level pricing and liquidity math in Uniswap v3 pools. The bug was caught and fixed before any known exploitation. However, forks that copied the pre-fix code remained vulnerable.

References:


Exploit 3: PRBMath / Fixed-Point Library Precision Bugs — Certora Disclosure (July 2023)

Root cause: Accumulated rounding errors in MULMOD-based fixed-point arithmetic libraries that compound across multiple operations.

Details: Certora used formal verification to analyze PRBMath, one of the most popular, gas-efficient fixed-point libraries in Solidity. PRBMath’s core operations (mulDiv, mul, div, exp, log) all rely on MULMOD for 512-bit intermediate precision. Certora found that while individual operations had small rounding errors (typically ≤1 ULP), these errors compounded across chains of operations in ways that unit tests could not detect:

  • A sequence of mul → div → mul → div operations accumulated enough error to create exploitable precision drift
  • In lending protocols using PRBMath for interest accrual, the drift could cause borrowers to owe slightly less than expected, enabling slow extraction of protocol funds
  • In AMMs, accumulated rounding errors in price calculations created tiny but profitable arbitrage opportunities

The disclosure noted that similar precision-loss vulnerabilities had already been exploited in:

  • Hundred Finance (April 2023, $6.8M): Exchange rate manipulation combined with integer rounding in a Compound V2 fork
  • Midas Capital (June 2023): Precision loss in borrowed amount calculations
  • Alpha Homora (February 2021): Rounding errors in leveraged yield farming calculations

MULMOD’s role: MULMOD provides the exact 512-bit intermediate that makes single-operation precision feasible. The bugs arise not from MULMOD itself but from the unavoidable truncation when the 512-bit intermediate must be reduced back to 256 bits. Each truncation loses at most 1 bit of precision, but n sequential operations can lose up to n bits — enough to matter in financial calculations with 18-decimal-place precision.

Impact: Affected every protocol using PRBMath (dozens of DeFi protocols). PRBMath was patched following the disclosure. The broader impact was a wave of precision-related exploits across Compound V2 forks throughout 2023.

References:


Exploit 4: Hundred Finance — Precision Loss in Exchange Rate (April 2023)

Root cause: Integer rounding vulnerability in redeemUnderlying() combined with exchange rate manipulation via donation, in a Compound V2 fork on Optimism.

Details: Hundred Finance’s hToken markets (forked from Compound V2) used fixed-point arithmetic with Solidity 0.5.16’s integer division for share-to-asset conversions. The attacker:

  1. Targeted an empty, unused hWBTC market (zero total supply, zero underlying)
  2. Minted a minimal number of hWBTC shares (1 or 2 wei)
  3. Donated a large amount of WBTC directly to the hToken contract, inflating the exchange rate to an extreme value
  4. Redeemed using redeemUnderlying(), where the integer division hTokenAmount = underlyingAmount * 1e18 / exchangeRate rounded down to 0 or 1 hWBTC for a claim on the entire underlying balance

The exchange rate calculation involved multiplication and division of large numbers — the same fundamental (a * b) / c pattern that mulDiv (and MULMOD internally) is designed to handle. But Compound V2’s original code predated modern mulDiv libraries and used standard Solidity checked arithmetic, which was vulnerable to precision loss at extreme exchange rates.

MULMOD’s relevance: This exploit demonstrates what happens when contracts do NOT use MULMOD-based full-precision arithmetic. The (amount * exchangeRate) / 1e18 calculation, performed with standard 256-bit arithmetic, lost critical precision when the exchange rate was astronomically inflated. A mulDiv implementation using MULMOD’s 512-bit intermediate would have maintained precision, though the fundamental donation-based manipulation would still need to be mitigated via minimum deposit requirements.

Impact: ~$6.8M drained from Optimism deployment (1,030 ETH, 1.2M+ USDC, 1.1M+ USDT). The attack cascaded across multiple hToken markets. Similar attacks hit Midas Capital and other Compound V2 forks, establishing precision loss as a systemic DeFi vulnerability class.

References:


Attack Scenarios

Scenario A: Dynamic Modulus Degenerating to Zero

contract VulnerableFieldMath {
    // Modulus is set by governance or oracle
    uint256 public fieldModulus;
 
    function setModulus(uint256 newModulus) external onlyOwner {
        fieldModulus = newModulus;  // No zero-check!
    }
 
    function fieldMultiply(uint256 a, uint256 b) external view returns (uint256) {
        // If fieldModulus == 0 (uninitialized, or set to 0 by mistake),
        // this returns 0 for ALL inputs -- every field element becomes zero
        return mulmod(a, b, fieldModulus);
    }
}

Attack: If governance sets fieldModulus = 0 (accidentally or via a compromised governor), all field multiplications return 0. In a signature verification context, this could make all signatures appear valid (since all computed values equal the zero point).

Scenario B: Rounding-Up Overflow in Custom mulDiv

library BuggyMath {
    function mulDivUp(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
        result = mulDiv(a, b, denominator);
        // BUG: no overflow check on the increment
        if (mulmod(a, b, denominator) > 0) {
            result += 1;  // Overflows if result == type(uint256).max
        }
    }
 
    function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
        // ... standard 512-bit division using mulmod ...
        uint256 prod0;
        uint256 prod1;
        assembly {
            let mm := mulmod(a, b, not(0))
            prod0 := mul(a, b)
            prod1 := sub(sub(mm, prod0), lt(mm, prod0))
        }
        require(prod1 < denominator);
        // ... remainder of division logic ...
    }
}
 
contract VulnerableVault {
    using BuggyMath for uint256;
 
    function previewWithdraw(uint256 assets) public view returns (uint256 shares) {
        // Round up shares needed (protocol-favorable)
        shares = assets.mulDivUp(totalSupply, totalAssets());
        // If this overflows to 0, user burns 0 shares for 'assets' worth of tokens
    }
}

Attack: Find assets and totalSupply/totalAssets values where mulDiv returns type(uint256).max and the remainder is nonzero. The +1 overflows to 0. The user burns 0 shares and withdraws assets for free.

Scenario C: MUL Used Instead of MULMOD in Curve Point Validation

contract VulnerableECVerifier {
    uint256 constant P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F; // secp256k1
 
    function isOnCurve(uint256 x, uint256 y) public pure returns (bool) {
        // Check: y^2 == x^3 + 7 (mod P)
 
        // BUG: Using MUL instead of MULMOD -- overflows silently!
        uint256 y2 = y * y;           // Should be: mulmod(y, y, P)
        uint256 x3 = x * x * x;      // Should be: mulmod(mulmod(x, x, P), x, P)
        uint256 rhs = x3 + 7;        // Should be: addmod(x3, 7, P)
 
        return y2 == rhs;  // Comparison is over truncated 256-bit values
    }
}

Attack: With MUL instead of MULMOD, both sides overflow and are reduced mod 2^256 instead of mod P. An attacker can submit invalid curve points that happen to satisfy the truncated equation, then exploit the verifier to forge signatures or bypass zk-proof checks.

Scenario D: Stack Ordering Bug in Assembly MULMOD

contract StackOrderBug {
    function computeFee(uint256 amount, uint256 rate, uint256 denominator) public pure returns (uint256) {
        uint256 result;
        assembly {
            // INTENDED: (amount * rate) % denominator
            // BUG: arguments swapped -- computes (amount * denominator) % rate
            result := mulmod(amount, denominator, rate)
        }
        return result;
    }
}

Attack: For amount = 1000, rate = 100, denominator = 10000:

  • Intended: (1000 * 100) % 10000 = 0 (exactly 1% fee)
  • Actual (buggy): (1000 * 10000) % 100 = 0 (happens to be 0 here, but with different values the results diverge completely)

The bug may produce correct-looking results for certain test inputs, passing unit tests while being fundamentally wrong.

Scenario E: Precision Drift in Chained mulmod Operations

contract PrecisionDrift {
    uint256 constant WAD = 1e18;
 
    function compoundInterest(
        uint256 principal,
        uint256 ratePerPeriod,
        uint256 periods
    ) public pure returns (uint256) {
        uint256 result = principal;
        for (uint256 i = 0; i < periods; i++) {
            // Each iteration loses up to 1 ULP of precision
            // Over 365 iterations (daily compounding for 1 year),
            // accumulated error can be significant
            uint256 interest = mulDiv(result, ratePerPeriod, WAD);
            result += interest;
        }
        return result;
        // After 365 iterations with ratePerPeriod = 0.01% (1e14),
        // accumulated error could be ~365 wei per 1e18 unit
        // On a 100M TVL protocol, this is ~36,500 wei per token = exploitable
    }
 
    function mulDiv(uint256 a, uint256 b, uint256 c) internal pure returns (uint256) {
        // Uses mulmod internally for 512-bit precision
        // But each call truncates, losing at most 1 bit
        // ...
    }
}

Mitigations

ThreatMitigationImplementation
T1: Modulus-by-zeroValidate modulus before every MULMODrequire(N != 0, "zero modulus") before mulmod(a, b, N)
T1: Dynamic modulusValidate modulus at write time, not just use timerequire(newModulus != 0 && newModulus != 1) in setter functions
T2: mulDiv precisionUse audited, formally verified mulDiv implementationsOpenZeppelin Math.mulDiv(), Solmate FullMath, or Uniswap v3 FullMath (post-fix)
T2: Rounding-up overflowCheck for type(uint256).max before incrementingrequire(result < type(uint256).max) before result++
T3: Wrong field modulusDefine modulus as constant or immutableuint256 constant FIELD_MODULUS = <prime> — cannot be changed after deployment
T3: MUL vs MULMOD confusionAlways use MULMOD for modular arithmetic; never substitute MULCode review checklist; static analysis rules to flag MUL in cryptographic functions
T4: Assembly stack errorsMinimize inline assembly; prefer Solidity mulmod() builtinUse assembly only in audited library functions; add extensive NatSpec and unit tests
T5: Share calculation roundingRound against the attacker (favor the protocol)Use mulDivUp for deposits (more shares needed), mulDivDown for withdrawals (fewer assets returned)
T5: Empty pool edge caseEnforce minimum deposits / non-zero total supply”Dead shares” pattern: mint initial shares to address(0) to prevent exchange rate manipulation
T6: Gas side channel in cryptoUse constant-time modular exponentiation or the ModExp precompilePrecompile at address 0x05 handles modular exponentiation with constant gas per input size
General: Precision driftBound the number of sequential mulDiv operations; use higher-precision intermediatesAccumulate interest via exponentiation (fewer operations) rather than iterative multiplication
General: Formal verificationVerify mulDiv correctness with SMT solversCertora, Halmos, or KEVM for proving precision bounds on MULMOD-based math

EVM-Level Protection: Correct N=0 Handling

All modern EVM clients correctly implement the Yellow Paper semantics (MULMOD(a, b, 0) = 0). The geth vulnerability was patched in v1.9.18 (August 2020). No workaround exists for older versions — upgrade is required.

EIP-5000: MULDIV Opcode (Proposed)

EIP-5000 proposes a native MULDIV opcode (0x1E) that computes ((x * y) / z) % 2^256 in a single instruction with 512-bit precision. This would replace the ~50-instruction MULMOD-based mulDiv pattern, eliminating an entire class of implementation bugs. The special case MULDIV(x, y, 0) would return the upper 256 bits of x * y, enabling efficient full-width multiplication. As of 2026, EIP-5000 remains in draft status.


Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighMediumgeth DoS (GHSA-jm5c-rv3w-w83m) — N=0 crashed nodes
T2Smart ContractHighMediumUniswap v3 mulDivRoundingUp overflow, PRBMath precision bugs (Certora 2023)
T3Smart ContractCriticalLowOn-chain EC verification bugs; theoretically devastating if exploited
T4Smart ContractHighMediumAssembly misuse in mulDiv forks and custom math libraries
T5Smart ContractHighMediumHundred Finance ($6.8M), Midas Capital — precision in share accounting
T6Smart ContractLowLowTheoretical for on-chain crypto; mitigated by ModExp precompile
P1ProtocolCritical (historical)N/A (patched)geth v1.9.16-v1.9.17 network partition risk
P2ProtocolLowN/A
P3ProtocolLowN/A

OpcodeRelationship
ADDMOD (0x08)Sibling modular arithmetic opcode; also uses extended precision intermediate (does not overflow). Frequently chained with MULMOD in field arithmetic: addmod(mulmod(a, b, P), c, P).
MUL (0x02)Standard 256-bit multiplication that truncates on overflow. MULMOD avoids this truncation via 512-bit intermediate. Used alongside MULMOD in the mulDiv pattern (prod0 = mul(a, b) for lower bits, mulmod(a, b, not(0)) for remainder).
MOD (0x06)Standard modulus on 256-bit values. Unlike MULMOD, MOD cannot recover information lost to MUL overflow — (a * b) % N via MUL+MOD is NOT equivalent to MULMOD when a * b > 2^256.
DIV (0x04)256-bit division, used in mulDiv implementations after the 512-bit product is reconstructed via MULMOD. Also returns 0 on division by zero (same silent-zero pattern as MULMOD with N=0).
EXP (0x0A)Modular exponentiation can be built from repeated MULMOD. However, the ModExp precompile (address 0x05) is preferred for large exponents due to gas efficiency and constant-time execution.