Precompile Summary
| Property | Value |
|---|---|
| Address | 0x05 |
| Name | MODEXP |
| Gas | Complex formula (EIP-2565): max(200, floor(multiplication_complexity * iteration_count / 3)). multiplication_complexity = max(Bsize, Msize)^2 / 8 where Bsize/Msize are base/modulus byte lengths. iteration_count derived from exponent length and leading zeros in the exponent. |
| Input | ABI-encoded (custom format): Bsize (32 bytes) | Esize (32 bytes) | Msize (32 bytes) | base (Bsize bytes) | exponent (Esize bytes) | modulus (Msize bytes) |
| Output | Result of base^exp mod modulus, zero-padded to Msize bytes |
| Introduced | Byzantium hardfork (EIP-198) |
| Behavior | Performs modular exponentiation on arbitrary-length big integers. Accepts variable-length base, exponent, and modulus fields with explicit length prefixes. Returns base^exp mod modulus zero-padded to Msize bytes. If modulus is 0, returns 0. Used for RSA signature verification, VDF computation, SNARK verification, and cryptographic accumulators. |
Threat Surface
MODEXP is Ethereum’s general-purpose big-integer modular exponentiation primitive. Unlike fixed-format precompiles such as ECRECOVER (128-byte input, 32-byte output), MODEXP accepts completely unbounded input sizes with a custom three-length-prefix encoding, making its threat surface uniquely broad: the combinatorial space of valid inputs is effectively infinite, and the relationship between input parameters and computation cost is non-linear and difficult to reason about.
The threat surface divides into four categories:
1. Gas cost manipulation through input crafting. The gas formula for MODEXP is the most complex of any precompile, depending on three independent dimensions (base size, exponent size, modulus size) plus the bit structure of the exponent itself (leading zeros reduce iteration_count). The original EIP-198 formula significantly underpriced certain input combinations, enabling attackers to craft inputs that consumed disproportionate computation relative to gas paid. EIP-2565 (Berlin, 2021) repriced the formula with a 200-gas minimum floor and revised the multiplication_complexity calculation, but the formula’s inherent complexity means edge cases may still exist where computation time exceeds gas cost expectations. EIP-7823 proposes capping inputs at 8192 bits (1024 bytes) to eliminate the long tail of adversarial input combinations entirely.
2. Consensus bugs from unbounded inputs and cross-client big-integer divergence. MODEXP’s acceptance of arbitrary-size inputs is, in the words of EIP-7823, “a source of numerous consensus bugs.” Each EVM client (geth, Nethermind, Besu, Erigon, Reth) implements big-integer arithmetic through different language-native libraries (Go’s math/big, Java’s BigInteger, Rust’s num-bigint), and these libraries handle edge cases differently — particularly around memory allocation for very large numbers, intermediate overflow during multiplication, modular reduction with unusual moduli, and behavior with leading zeros. Inputs with impractical sizes (e.g., 10,000-byte modulus) are valid at the protocol level but exercise code paths that no legitimate use case ever touches.
3. Silent failure semantics on degenerate inputs. MODEXP never reverts. When modulus is 0, it returns an all-zero output of the requested size rather than signaling an error. When the exponent is 0, it returns 1 (or 0 if modulus is 1). When input data is shorter than the declared lengths, missing bytes are zero-padded, which can silently transform the intended computation — most critically, a truncated exponent becomes 0, and base^0 = 1 for any base, which can bypass RSA verification checks.
4. Custom encoding that defies standard tooling. MODEXP does not use standard ABI encoding. Its three-length-prefix format is bespoke to EIP-198, meaning standard Solidity abi.encode/abi.decode cannot be used. Developers must manually construct the input, creating opportunities for off-by-one errors in length calculations, misaligned field boundaries, and incorrect padding that silently produces wrong results rather than reverting.
Smart Contract Threats
T1: Gas Cost Manipulation and DoS (High)
The MODEXP gas formula is the most complex of any EVM precompile, with cost depending on three independent size parameters and the bit structure of the exponent. This complexity creates opportunities for attackers to craft inputs that maximize computation time relative to gas paid.
-
Original EIP-198 underpricing. Before Berlin (April 2021), the gas formula was based on a Karatsuba multiplication approximation that did not accurately reflect actual computation time on modern hardware. Certain input combinations — large exponents with small bases, or specific modulus sizes that hit worst-case paths in big-integer libraries — could consume far more wall-clock time than the gas price implied. This was a known DoS vector against Ethereum nodes.
-
EIP-2565 repricing. The Berlin hardfork (EIP-2565) replaced the
multiplication_complexityformula, changing fromceil(max(Bsize, Msize) / 8)^2tomax(Bsize, Msize)^2 / 8, and added a 200-gas minimum floor. This significantly reduced the underpricing gap but did not eliminate it entirely — the formula remains an approximation of actual computation cost. -
Residual edge cases. The gas formula assumes a relationship between input size and computation time that holds for typical inputs but may break down for adversarial ones. Inputs where Bsize and Esize are large but Msize is small, or where the exponent has many leading zeros (reducing
iteration_countand thus gas cost) but the actual computation remains expensive due to memory allocation overhead, can still create disproportionate load. -
EIP-7823 input caps. The proposed EIP-7823 would cap each input field (Bsize, Esize, Msize) at 1024 bytes (8192 bits). Analysis of historical mainnet usage shows no successful MODEXP call has ever used inputs exceeding 513 bytes. This cap would dramatically reduce the adversarial input space and eliminate the long tail of gas mispricing edge cases.
Why it matters: A DoS attack against MODEXP does not steal funds directly but can halt block production or cause nodes to fall behind the chain head. In a proof-of-stake context, this can cause validators to miss attestations, resulting in slashing or reduced rewards. The complexity of the gas formula makes it difficult for node operators to predict and defend against adversarial inputs.
T2: Consensus Bugs from Unbounded Inputs (Critical)
MODEXP accepts completely unbounded input sizes. Every EVM client must produce bit-identical results for any valid input, but the big-integer libraries underlying each client’s implementation handle edge cases differently, creating persistent consensus risk.
-
Cross-client divergence. Geth uses Go’s
math/big, Nethermind uses .NET’sBigInteger, Besu uses Java’sBigInteger, and Reth/Erigon use Rust’snum-bigint. Each library has different internal representations, different algorithms for multiplication and modular reduction, and different behavior at extreme sizes. A subtle difference in how any library handles, for example, a 5,000-byte modulus with a 1-byte base can cause one client to produce a different result than another — a consensus split. -
EIP-7823 motivation. The EIP explicitly states that unbounded MODEXP inputs have been “a source of numerous consensus bugs” across client implementations. The proposal to cap inputs at 1024 bytes is motivated primarily by reducing the testing surface: instead of needing to verify correctness across an infinite input space, clients would only need to cover inputs up to a known bound.
-
Memory allocation edge cases. Very large Bsize, Esize, or Msize values force clients to allocate large memory buffers for intermediate computation. Different clients handle this differently — some pre-allocate, some grow dynamically, some have internal size limits. An input that triggers an out-of-memory condition in one client but not another is a consensus bug.
-
Leading zeros and iteration count. The gas formula grants a discount for leading zeros in the exponent (they reduce
iteration_count), but the actual modular exponentiation must still process the full exponent value. If a client’s implementation incorrectly handles leading zeros differently from trailing zeros during the exponentiation loop, results can diverge.
Why it matters: Consensus bugs are the most severe class of Ethereum vulnerability. A single transaction that produces different results on different clients can split the network, with validators on different forks attesting to incompatible states. The unbounded nature of MODEXP inputs means the space of potential consensus-triggering inputs is effectively infinite, making exhaustive testing impossible.
T3: Silent Failure on Modulus Zero (Medium)
When the modulus is 0, MODEXP returns an all-zero output of Msize bytes rather than reverting. Contracts that do not validate the modulus before calling MODEXP may silently receive incorrect results that appear valid.
-
RSA verification bypass. In RSA signature verification, the modulus is the RSA public key’s
nvalue. If an attacker can supply a zero modulus (e.g., through a parameter injection or an unvalidated user-supplied key), MODEXP returns 0. If the expected hash or verification value is also 0 (or if the comparison is weak), the verification silently “succeeds.” -
Division-by-zero analogy. Mathematically,
base^exp mod 0is undefined. MODEXP’s choice to return 0 rather than revert follows a convention of returning a “safe” default, but this violates fail-fast principles. Contracts expecting a meaningful result receive one that looks valid (it has the right length, it’s a valid byte sequence) but is semantically wrong. -
VDF verification. Verifiable Delay Functions that use modular exponentiation are similarly vulnerable: a zero modulus causes the VDF verification to return 0, which may match a precomputed “expected output” of 0 in a poorly designed verification scheme.
Why it matters: Silent failures are more dangerous than loud failures. A revert would halt execution and signal an error; a zero return passes silently through downstream logic, potentially authorizing actions that should have been rejected.
T4: Input Encoding Vulnerabilities (Medium)
MODEXP uses a bespoke encoding format that is not compatible with Solidity’s standard ABI encoding. The three length prefixes (Bsize, Esize, Msize) followed by raw data create multiple opportunities for encoding errors that silently alter the computation.
-
Truncated input / zero-padding. If the actual calldata is shorter than
96 + Bsize + Esize + Msize, missing bytes are zero-padded. This means a maliciously short input can cause the exponent to be interpreted as 0. Sincebase^0 = 1for any non-zero base (and1 mod modulusproduces a small result), this can completely change the computation’s output without any error signal. -
Misaligned length fields. If a contract constructs the MODEXP input manually and makes an off-by-one error in the length fields, the precompile reads the wrong bytes for base, exponent, and modulus. The computation proceeds without error on the misaligned data, producing a valid-looking but completely wrong result.
-
Oversized length fields. If Bsize, Esize, or Msize are set to extremely large values (e.g., 2^32), the gas cost becomes astronomical, but if a contract computes gas incorrectly and provides enough gas, the precompile will attempt to allocate memory for these sizes. In practice, the gas cost prevents this from being an on-chain attack, but it can DoS off-chain simulation or
eth_callendpoints. -
No standard library support. Unlike ECRECOVER (which has
ecrecover()built into Solidity), MODEXP must be called via low-levelstaticcall(gas, 0x05, input, inputLen, output, outputLen). This forces developers to manually handle memory layout, increasing the risk of encoding errors.
Why it matters: Custom encoding formats are a persistent source of bugs in Ethereum development. The absence of standard tooling for MODEXP means every integration must hand-roll the encoding, and encoding errors produce silent incorrect results rather than reverts.
Protocol-Level Threats
P1: EIP-2565 Gas Repricing — Necessary but Incomplete
EIP-2565 (Berlin, April 2021) was a critical fix for MODEXP gas pricing. The original EIP-198 formula used a Karatsuba multiplication approximation (ceil(max(Bsize, Msize) / 8)^2) that did not reflect actual computation costs. EIP-2565 changed the formula to max(Bsize, Msize)^2 / 8 and added a 200-gas minimum floor.
The repricing reduced the most egregious DoS vectors but did not eliminate the fundamental problem: the gas formula is an approximation of computation cost, and approximations have error margins. The relationship between input parameters and actual CPU time is implementation-dependent (Go’s math/big has different performance characteristics than Rust’s num-bigint), so a formula that is accurate for one client may overprice or underprice for another. The 200-gas minimum floor prevents trivially cheap calls but does not address the long tail of adversarial inputs where the formula’s approximation breaks down.
P2: EIP-7823 Input Size Cap — Eliminating the Long Tail
EIP-7823 proposes capping each MODEXP input field (Bsize, Esize, Msize) at 8192 bits (1024 bytes). Historical analysis shows that no successful MODEXP call on mainnet has ever used inputs exceeding 513 bytes. The 1024-byte cap provides a generous safety margin for all known use cases (RSA-2048 needs 256 bytes, RSA-4096 needs 512 bytes) while eliminating the infinite tail of inputs that serve no legitimate purpose but create consensus risk.
This proposal would also enable clients to use fixed-size or bounded-size big-integer implementations with known performance characteristics, rather than arbitrary-precision libraries that may have unpredictable behavior at extreme sizes. The reduced input space makes exhaustive fuzz testing tractable, dramatically improving confidence in cross-client consistency.
P3: Client Implementation Divergence — The Persistent Consensus Risk
Each EVM client’s MODEXP implementation depends on its language’s big-integer library, and these libraries differ in subtle ways:
-
Memory allocation strategies. Go’s
math/biggrows buffers dynamically, Java’sBigIntegeris immutable (creating new objects for each operation), and Rust’snum-bigintuses a custom allocator. Under extreme input sizes, these differences can manifest as different timing behavior (relevant for DoS) or, in rare cases, different results due to intermediate overflow handling. -
Modular exponentiation algorithms. Libraries may use different algorithms (binary exponentiation, Montgomery multiplication, Barrett reduction) and switch between them at different input-size thresholds. If two clients use different algorithms for the same input, and one algorithm has a bug at a specific input size, the results diverge.
-
Leading zero handling. The gas formula treats leading zeros in the exponent specially (they reduce
iteration_count), but the modular exponentiation itself must produce identical results regardless of leading zeros. A client that strips leading zeros before computing (changing the internal representation) must produce the same result as one that retains them.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
Bsize=0, Esize=0, Msize=0 | Returns empty output (0 bytes); costs 200 gas (minimum floor) | No direct security impact, but contracts expecting a non-empty return may misinterpret the result |
| Modulus = 0 | Returns Msize zero bytes | Silent failure: base^exp mod 0 is undefined, but returns 0. RSA or VDF verification may silently pass with a zero expected value (T3) |
| Exponent = 0, modulus > 1 | Returns 1 (base^0 = 1 mod modulus for modulus > 1) | A truncated input that zero-pads the exponent will always return 1, potentially bypassing verification checks (T4) |
| Exponent = 0, modulus = 1 | Returns 0 (1 mod 1 = 0) | Corner case: the combination of zero exponent and unit modulus produces 0, not 1 |
| Base = 0 | Returns 0 (0^exp = 0 for any exp > 0) | No direct vulnerability, but contracts should handle this if they expect non-zero results |
| Modulus = 1 | Returns 0 (anything mod 1 = 0) | All outputs are 0 regardless of base and exponent; could mask computation errors |
| Very large Bsize/Esize/Msize (e.g., >1024 bytes) | Valid; high gas cost; stresses client memory allocation | Primary consensus bug vector: clients may diverge on memory handling or intermediate computation for extreme sizes (T2) |
| Leading zeros in exponent | Reduces gas cost (iteration_count considers leading zeros) but does not change the result | Attacker can reduce gas cost for the same computation by padding the exponent with leading zeros, though this also increases Esize (and thus multiplication_complexity), partially offsetting the discount |
| Input shorter than 96 bytes | Length fields (Bsize, Esize, Msize) are zero-padded; all become 0 | Returns empty output (0 bytes); costs 200 gas minimum |
Input shorter than 96 + Bsize + Esize + Msize | Missing bytes are zero-padded | Exponent may be truncated to 0 (base^0 = 1), silently changing the computation result (T4) |
| Bsize or Esize or Msize = 2^256 - 1 | Gas cost overflows or is astronomically high; call will fail due to insufficient gas | Not directly exploitable on-chain but can DoS off-chain simulation or eth_call endpoints |
Real-World Exploits
Exploit 1: Persistent Consensus Bugs Across Client Implementations (2017—Present)
Root cause: MODEXP’s acceptance of unbounded inputs forces every EVM client to produce identical results for an infinite input space using different big-integer libraries, an inherently fragile proposition.
Details: Since MODEXP’s introduction in Byzantium (October 2017), multiple consensus bugs have been discovered in various client implementations when processing unusual MODEXP inputs. The EIP-7823 motivation section explicitly cites unbounded MODEXP inputs as “a source of numerous consensus bugs.” These bugs typically involve edge cases where one client’s big-integer library handles an unusual input differently from another’s — for example, very large moduli that trigger different internal multiplication algorithms, or inputs where the declared sizes cause memory allocation to behave differently across language runtimes.
No single catastrophic exploit has occurred because these bugs were typically caught during testing or on testnets before reaching mainnet. However, the persistent discovery of such bugs demonstrates that the current design creates ongoing consensus risk that cannot be fully mitigated through testing alone — the input space is simply too large to test exhaustively.
MODEXP’s role: The precompile’s acceptance of arbitrary-size inputs is the root cause. Each new edge case discovered represents a potential consensus split that was one missed test away from splitting the network.
Impact: No confirmed mainnet consensus split, but persistent engineering cost across all client teams to discover and fix MODEXP edge cases. The ongoing risk motivated EIP-7823’s proposal to cap input sizes.
References:
- EIP-7823: Set upper bounds for MODEXP
- EIP-2565: MODEXP Gas Cost Repricing
- EIP-198: Big Integer Modular Exponentiation (original)
Exploit 2: EIP-198 Gas Mispricing DoS Vector (Pre-Berlin, 2021)
Root cause: The original MODEXP gas formula (EIP-198) significantly underpriced certain input combinations, allowing an attacker to consume disproportionate node computation relative to gas paid.
Details: Before the Berlin hardfork (April 2021), the MODEXP gas formula used a Karatsuba multiplication approximation that underestimated the actual computation cost for certain input profiles. Specifically, inputs with large exponents and small-to-medium moduli could trigger expensive modular exponentiation that took significantly more wall-clock time than the gas formula predicted. Security researchers demonstrated that carefully crafted MODEXP calls could slow down block processing on affected nodes.
The attack was conceptually similar to the “Shanghai DoS attacks” of September 2016 (which targeted other underpriced opcodes like EXTCODESIZE), but MODEXP’s complex multi-dimensional gas formula made it harder to identify and fix the mispricing. EIP-2565 addressed this by revising the formula and adding a 200-gas minimum floor.
MODEXP’s role: The precompile’s multi-dimensional gas formula created a large surface of input combinations where gas cost did not accurately reflect computation time. The formula’s complexity made it difficult to audit and verify correct pricing across all input profiles.
Impact: No confirmed mainnet exploitation, but the vulnerability was demonstrated by security researchers and motivated the EIP-2565 repricing in Berlin.
References:
Attack Scenarios
Scenario A: DoS via Adversarial Input Sizing
contract ModexpDoS {
address constant MODEXP = address(0x05);
// Attacker crafts inputs that maximize computation relative to gas cost.
// Large exponent with leading zeros reduces iteration_count (and gas),
// but the actual big-integer operations remain expensive due to memory
// allocation and multiplication overhead for large Bsize and Msize.
function attack() external {
uint256 Bsize = 512;
uint256 Esize = 512;
uint256 Msize = 512;
bytes memory input = new bytes(96 + Bsize + Esize + Msize);
assembly {
let ptr := add(input, 32)
mstore(ptr, Bsize)
mstore(add(ptr, 32), Esize)
mstore(add(ptr, 64), Msize)
}
// Fill base with 0xFF bytes (large value)
for (uint256 i = 96; i < 96 + Bsize; i++) {
input[i] = 0xFF;
}
// Exponent: many leading zeros (reduces gas cost via iteration_count)
// followed by a large value at the end
input[96 + Bsize + Esize - 1] = 0xFF;
// Fill modulus with 0xFF bytes (large prime-like value)
for (uint256 i = 96 + Bsize + Esize; i < 96 + Bsize + Esize + Msize; i++) {
input[i] = 0xFF;
}
// The gas cost is reduced by leading zeros in the exponent,
// but computation time may not decrease proportionally
(bool success, ) = MODEXP.staticcall(input);
require(success);
}
}Scenario B: RSA Verification Bypass via Zero Modulus
contract VulnerableRSAVerifier {
address constant MODEXP = address(0x05);
struct RSAPublicKey {
bytes modulus; // n
bytes exponent; // e (typically 65537)
}
mapping(address => RSAPublicKey) public registeredKeys;
// VULNERABLE: does not validate that modulus is non-zero
function verifyRSASignature(
address signer,
bytes memory message,
bytes memory signature
) external view returns (bool) {
RSAPublicKey storage key = registeredKeys[signer];
// If signer never registered a key, modulus and exponent are empty (zero)
uint256 Bsize = signature.length; // base = signature
uint256 Esize = key.exponent.length;
uint256 Msize = key.modulus.length;
bytes memory input = abi.encodePacked(
bytes32(Bsize),
bytes32(Esize),
bytes32(Msize),
signature,
key.exponent,
key.modulus
);
(bool success, bytes memory result) = MODEXP.staticcall(input);
require(success, "modexp failed");
// VULNERABLE: if modulus is empty/zero, result is all zeros
// If expected hash is also zero (e.g., hash of empty message),
// verification passes
bytes32 expectedHash = sha256(message);
bytes32 recovered;
if (result.length >= 32) {
assembly {
recovered := mload(add(result, 32))
}
}
return recovered == expectedHash;
}
}
// Attack:
// 1. Find a signer address with no registered RSA key (modulus = empty, exponent = empty)
// 2. Craft a message whose sha256 hash has leading zeros (or is exactly 0x00...00)
// 3. Call verifyRSASignature with any signature bytes
// 4. MODEXP returns 0 (Msize=0, empty result) or zeros (if modulus was zero-valued)
// 5. If expectedHash matches the zero result, verification "succeeds"Scenario C: Input Truncation Causing Exponent Zeroing
contract VulnerableVDF {
address constant MODEXP = address(0x05);
// VDF verification: checks that output^(2^T) mod N == input
// Uses MODEXP to compute output^(2^T) mod N
function verifyVDF(
bytes memory output,
bytes memory proofExponent, // should be 2^T
bytes memory modulus,
bytes memory expectedInput
) external view returns (bool) {
uint256 Bsize = output.length;
uint256 Esize = proofExponent.length;
uint256 Msize = modulus.length;
// VULNERABLE: if caller provides proofExponent shorter than declared Esize,
// the exponent is zero-padded, potentially becoming 0
bytes memory input = new bytes(96 + Bsize + Esize + Msize);
assembly {
let ptr := add(input, 32)
mstore(ptr, Bsize)
mstore(add(ptr, 32), Esize)
mstore(add(ptr, 64), Msize)
}
// Copy base (output)
for (uint256 i = 0; i < output.length; i++) {
input[96 + i] = output[i];
}
// VULNERABLE: if proofExponent.length < Esize,
// remaining bytes in the exponent field are 0x00
// If proofExponent is entirely missing, exponent = 0, so result = 1
for (uint256 i = 0; i < proofExponent.length && i < Esize; i++) {
input[96 + Bsize + i] = proofExponent[i];
}
// Copy modulus
for (uint256 i = 0; i < modulus.length; i++) {
input[96 + Bsize + Esize + i] = modulus[i];
}
(bool success, bytes memory result) = MODEXP.staticcall(input);
require(success, "modexp failed");
return keccak256(result) == keccak256(expectedInput);
}
}
// Attack:
// 1. Submit a VDF "proof" with proofExponent = empty bytes (length 0)
// 2. Contract sets Esize to 0, so exponent field is empty
// 3. MODEXP computes output^0 mod modulus = 1
// 4. If expectedInput == bytes representation of 1 (or attacker controls expectedInput),
// VDF verification is bypassed without performing any actual delay computationMitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Gas cost DoS | Cap input sizes | Enforce require(Bsize <= 1024 && Esize <= 1024 && Msize <= 1024) before calling MODEXP; aligns with EIP-7823 proposed limits |
| T1: Gas cost DoS | Set explicit gas limits on MODEXP calls | Use MODEXP.staticcall{gas: maxGas}(input) with a pre-calculated gas budget based on expected input sizes; do not forward all available gas |
| T2: Consensus bugs | Limit input sizes at the contract level | Same as T1: cap Bsize, Esize, Msize to the minimum required for the application (e.g., 256 bytes for RSA-2048) |
| T3: Zero modulus | Validate modulus before calling MODEXP | require(modulus.length > 0 && !isZero(modulus), "invalid modulus") where isZero checks that at least one byte is non-zero |
| T3: Zero result handling | Validate MODEXP output is meaningful | Check that the result is within the expected range for the application; do not accept all-zero results without explicit validation |
| T4: Input truncation | Validate input lengths match actual data | require(base.length == Bsize && exponent.length == Esize && modulus.length == Msize) before constructing the MODEXP input |
| T4: Encoding errors | Use a well-tested MODEXP wrapper library | Use audited libraries that handle the custom encoding format rather than hand-rolling staticcall input construction |
| General | Prefer native Solidity operations for small values | For modular exponentiation with values fitting in 256 bits, use mulmod and exponentiation-by-squaring in Solidity instead of the precompile |
Compiler/EIP-Based Protections
- EIP-198 (Byzantium, October 2017): Introduced MODEXP as a precompile for arbitrary-precision modular exponentiation. Original gas formula based on Karatsuba multiplication approximation.
- EIP-2565 (Berlin, April 2021): Repriced the MODEXP gas formula. Changed
multiplication_complexitycalculation fromceil(max(Bsize, Msize) / 8)^2tomax(Bsize, Msize)^2 / 8. Added a 200-gas minimum floor. Addressed the most egregious DoS vectors from the original pricing. - EIP-7823 (Proposed): Caps each MODEXP input field (Bsize, Esize, Msize) at 8192 bits (1024 bytes). Motivated by persistent consensus bugs from unbounded inputs and the observation that no legitimate mainnet usage has exceeded 513 bytes. Would dramatically reduce the testing surface and eliminate the long tail of adversarial input combinations.
- Solidity
addmod/mulmod: For modular arithmetic on values that fit in 256 bits, Solidity’s nativeaddmod(a, b, N)andmulmod(a, b, N)are simpler, cheaper, and safer than calling the MODEXP precompile.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | Medium | Original EIP-198 gas formula was demonstrably underpriced for adversarial inputs. EIP-2565 mitigated but did not fully eliminate the risk. EIP-7823 proposes further hardening via input caps. |
| T2 | Protocol | Critical | Medium | Multiple consensus bugs discovered across client implementations due to unbounded MODEXP inputs. EIP-7823 cites this as the primary motivation for input size caps. No confirmed mainnet split, but persistent risk. |
| T3 | Smart Contract | Medium | Low | No known exploits, but the pattern of silent failure on zero modulus mirrors the address(0) return problem in ECRECOVER (which has been widely exploited). RSA and VDF verification contracts are at risk. |
| T4 | Smart Contract | Medium | Medium | Custom encoding format is a persistent source of integration bugs. Truncated inputs silently zeroing the exponent is a subtle and dangerous failure mode. |
| P1 | Protocol | High | N/A | EIP-2565 was a necessary repricing that fixed known DoS vectors. The complexity of the gas formula means residual edge cases may exist. |
| P2 | Protocol | High | Low | Proposed, not yet adopted. If accepted, EIP-7823 would significantly reduce consensus risk and simplify client implementations. |
| P3 | Protocol | Critical | Medium | Ongoing risk. Each new client version or new big-integer library update can reintroduce divergence bugs for edge-case MODEXP inputs. |
Related Precompiles
| Precompile | Relationship |
|---|---|
| ECRECOVER (0x01) | Both are cryptographic verification primitives. ECRECOVER handles ECDSA signatures (secp256k1), while MODEXP enables RSA signature verification via modular exponentiation. Contracts performing RSA verification use MODEXP where they would use ECRECOVER for ECDSA. Both share the pattern of silent failure (ECRECOVER returns address(0), MODEXP returns 0 on zero modulus). |
| IDENTITY (0x04) | The identity precompile (memory copy) is sometimes used in pipelines that prepare input data for MODEXP, particularly when copying large byte arrays into the custom encoding format required by the precompile. |
| BN256ADD (0x06), BN256MUL (0x07), BN256PAIRING (0x08) | These bn256 curve precompiles serve similar cryptographic verification purposes (SNARK verification, BLS signatures) but operate on elliptic curve points rather than arbitrary big integers. MODEXP and the bn256 precompiles are often used together in SNARK verification circuits. All share the challenge of cross-client implementation consistency for complex mathematical operations. |