Precompile Summary
| Property | Value |
|---|---|
| Address | 0x07 |
| Name | BN256MUL (also called ECMUL, alt_bn128 scalar multiplication) |
| Gas | 6,000 (post-EIP-1108, Istanbul). Was 40,000 pre-Istanbul. |
| Input | 96 bytes: point x (32 bytes), point y (32 bytes), scalar s (32 bytes) |
| Output | 64 bytes: resulting point (x', y') = s * (x, y) on the curve |
| Behavior | Performs scalar multiplication of a point on the alt_bn128 (BN254) curve by a scalar. The input point must lie on the curve or be the point at infinity; otherwise the call fails (empty return). Introduced in Byzantium (EIP-196). Used for zkSNARK verification, BLS-style operations built on the pairing curve, and Pedersen commitments. |
Threat Surface
BN256MUL sits on the hot path for many zero-knowledge and cryptographic protocols: any verifier that multiplies curve points by scalars before pairing checks, Pedersen-style openings, or similar gadgets depends on this precompile. Its threat surface combines correctness under adversarial inputs, cross-client consensus, and how Solidity and low-level callers interpret failure.
Unlike pure hash precompiles, scalar multiplication amplifies small errors in point handling. If an implementation accepts a point that is not on the curve (or validates the wrong algebraic property, such as subgroup checks without full on-curve verification), the computed s * P can diverge from the specification and from other clients. That divergence is especially dangerous when the output feeds BN256PAIRING (0x08) or other steps that assume points are honest curve elements: an off-curve intermediate can make verification accept or reject proofs incorrectly relative to the intended cryptography.
At the smart-contract layer, the EVM still charges fixed gas (6,000 post-Istanbul) regardless of the scalar’s Hamming weight, while real wall-clock time in the client may vary with the scalar’s bit pattern—a narrow timing side channel that rarely matters for on-chain logic but can matter for co-located infrastructure. More practically, contracts that ignore CALL success or return data length risk reusing stale memory as if it were a fresh curve point, which is catastrophic when that value is later used in pairing-based soundness arguments.
Smart Contract Threats
T1: Point Validation Bypass — CVE-2025-30147 (Critical)
A Besu vulnerability in the gnark-crypto path used for BN256 curve precompiles (including BN256MUL) showed that the implementation did not correctly ensure inputs were on the alt_bn128 curve, relying on checks that were insufficient (e.g., subgroup-oriented validation without proper on-curve verification, analogous to the BN256ADD issue). Scalar multiplication of an off-curve point yields results that do not match a correct client: the “error” is not a clean revert at the EVM boundary but wrong curve points that can flow into downstream verification.
- Amplification. Addition mis-handles one bad point; scalar multiplication mixes that point through a full double-and-add style schedule, so incorrect assumptions about
Pproduce outputs that are wrong in ways that are hard to predict from local tests alone. - Consensus and cross-chain risk. Any client that accepts points another client rejects (or vice versa) risks consensus failure; in the CVE window, the practical impact was divergent precompile output vs. correct implementations for malformed-but-subgroup-passing inputs.
- zkSNARK verifier blast radius. Verifiers rarely call BN256MUL in isolation; they combine it with BN256ADD and BN256PAIRING. A single bad intermediate breaks the algebraic relations the proof system assumes.
Why it matters: This class of bug is not “fails safe”; it can make valid proofs fail or invalid proofs appear valid depending on how the contract combines results, undermining the security story of the entire proof gadget.
T2: Timing Side-Channel Concerns (Low)
Scalar multiplication algorithms often exhibit data-dependent execution time (e.g., number of additions depends on which bits of the scalar are set). EVM gas for BN256MUL is fixed at 6,000 (post-Istanbul), so on-chain economic cost does not leak the scalar. However, node-local wall-clock time may still correlate with the scalar value.
- Exploitability in practice. Exploiting this from a smart contract is extremely limited: miners/validators do not typically expose fine-grained timing of precompile execution to arbitrary callers in a reliable way. Risk is mostly relevant to shared infrastructure, benchmarking, or co-located workloads—not to typical Solidity business logic.
- Still worth documenting for completeness when threat-modeling privacy-critical systems that also run off-chain provers next to validating nodes.
Why it matters: Low severity for EVM contracts, but it is a real property of many implementations and belongs in a complete precompile threat model.
T3: Unchecked Return Value / Stale Memory (Medium)
The same pattern as BN256ADD: if a contract uses assembly, inline Yul, or low-level call to 0x07 and does not assert success and 64-byte return data, Solidity may leave previous memory contents in place. The caller can then treat garbage or old (x, y) as the result of s * P.
- Downstream pairing checks. Stale coordinates are not random noise in a well-designed verifier; they are structured field elements that can interact badly with pairing equations and library assumptions.
- Harder to spot than a revert. High-level
staticcallwrappers that ignore the boolean return are a recurring audit theme; scalar multiplication makes the consequences worse because the bad values feed multi-step protocols.
Why it matters: A missed success check turns a deterministic cryptographic primitive into an uninitialized-memory bug with proof-system impact.
Protocol-Level Threats
P1: Gas repricing (EIP-1108, Istanbul)
EIP-1108 reduced BN256MUL gas from 40,000 to 6,000 (~85% drop), making zkSNARK verification and related gadgets roughly 6.7× cheaper in the curve-arithmetic portion of typical verifiers. This unlocked privacy protocols and L2 systems that were previously gas-prohibitive.
- Tradeoff. Cheaper verification increases on-chain attack surface density: more contracts can afford full verifiers, so implementation bugs (client or contract) affect more value and more protocols.
P2: Library migration and insufficient validation coverage (CVE-2025-30147)
CVE-2025-30147 illustrates that switching the underlying library (e.g., to gnark-crypto) can introduce subtle validation gaps not caught by legacy test vectors, which often assume honest or only mildly malformed inputs. Subgroup checks, encoding conventions, and point-at-infinity handling differ in subtle ways across libraries.
- Lesson. Consensus-critical cryptography needs negative testing and cross-client differential fuzzing, especially when replacing implementations.
P3: Fixed gas vs. data-dependent real cost
The 6,000 gas is independent of the scalar value. Worst-case scalars (many non-zero bits) induce more group operations than sparse scalars in typical implementations, so worst-case work can be underpriced relative to best-case scalars at the protocol level.
- Impact. Primarily an economic / DoS margin concern for block producers (more work per gas in pathological cases), not a direct soundness bug. It interacts with global block gas limits and scheduling but does not by itself break curve math.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
Point at infinity represented as (0, 0) | (0, 0) * s → (0, 0) for any scalar | Callers must treat (0,0) as the designated infinity encoding per spec; mishandling breaks verifier logic. |
P * 0 | Yields the point at infinity (0, 0) | Scripts must not assume a non-infinity output when scalar is zero. |
P * 1 | Returns P | Identity case; useful for testing but easy to confuse with unchecked-return bugs if success is not checked. |
P * n where n is the curve order | Yields (0, 0) (infinity) | Consistent with group law; verifiers must expect infinity outputs in honest proofs. |
Scalar ≥ curve order | Reduced modulo the curve order | Callers must not assume scalars are already canonically reduced unless they enforce it off-chain. |
| Input shorter than 96 bytes | Right-padded with zeros (as for other precompiles) | Truncated or padded inputs can produce unexpected points or scalars; treat as untrusted encoding. |
| Point not on curve (and not valid infinity encoding) | Call fails: empty return data | If success is not checked, stale memory may be used (T3). |
Real-World Exploits
CVE-2025-30147 (Critical) — Besu / gnark-crypto validation gap
Root cause: Incorrect or insufficient on-curve validation in the gnark-crypto-based implementation of BN256 precompiles (shared context with BN256ADD); off-curve points could be processed in a way that diverged from correct clients.
BN256MUL’s role: Scalar multiplication amplifies mishandled points and feeds pairing-heavy verifiers; impact is not isolated to a single precompile call.
Impact: Consensus-threatening client divergence and broken cryptographic assumptions for zkSNARK and related on-chain verifiers relying on 0x06 / 0x07 / 0x08.
Note: There are no well-known exploits limited only to BN256MUL in isolation; practical fallout is systemic to any gadget that trusts these precompiles.
Attack Scenarios
Scenario A: BN256MUL without checking success — stale memory
// VULNERABLE PATTERN (illustrative Yul-style usage)
pragma solidity ^0.8.0;
contract UnsafeBn256Mul {
function mulAndUse(
bytes32 x,
bytes32 y,
bytes32 s
) external view returns (bytes32 outX) {
bytes memory input = abi.encodePacked(x, y, s);
bytes memory output = new bytes(64);
assembly {
let success := staticcall(
gas(),
0x07,
add(input, 32),
96,
add(output, 32),
64
)
// VULNERABLE: success ignored; if staticcall fails, `output` may be
// all zeroes or retain prior memory contents depending on compiler/runtime.
outX := mload(add(output, 32))
}
// Attacker triggers failure (e.g., off-curve point) and relies on
// `outX` still being used in a pairing check elsewhere.
}
}Scenario B: Pedersen-style check that assumes success without on-curve proofs
// CONCEPTUAL: commitment verification that trusts BN256MUL output blindly
pragma solidity ^0.8.0;
contract RiskyPedersenCheck {
// G, H assumed fixed generators on alt_bn128 (illustrative only)
function verifyOpening(
bytes32 Px,
bytes32 Py,
bytes32 s,
bytes32 expectedX,
bytes32 expectedY
) external view returns (bool ok) {
bytes memory input = abi.encodePacked(Px, Py, s);
bytes memory result = new bytes(64);
(bool success,) = address(0x07).staticcall(input);
// VULNERABLE: does not require(success) or result.length == 64
assembly {
returndatacopy(add(result, 32), 0, returndatasize())
}
bytes32 rx;
bytes32 ry;
assembly {
rx := mload(add(result, 32))
ry := mload(add(result, 64))
}
// If the call failed, rx/ry may be unrelated to s * P — yet compared
// to expected values or fed into a pairing-based relation.
ok = (rx == expectedX && ry == expectedY);
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Point validation / client bugs | Run patched consensus clients; monitor advisories | Treat CVE-2025-30147-class issues as node ops fixes; contracts cannot patch the client. |
| T1: Protocol design | Avoid trusting single-client behavior on malformed inputs | Favor standardized encoding, explicit rejection of invalid points at the contract boundary where possible, and cross-client testing before mainnet deployment. |
| T2: Timing | Isolate sensitive workloads | Do not rely on secret scalars in environments where micro-architectural timing attacks are in scope; keep secrets off the hot path next to validators if threat model requires it. |
| T3: Unchecked return | Always check success and 64-byte output | require(success && returndatasize() == 64); then copy return data; in Solidity 0.8+, prefer typed wrappers that revert on failure. |
| T3: Memory hygiene | Zero or overwrite buffers after failed calls | When using assembly, explicitly mstore zeros on failure paths before branching. |
| General | Use audited libraries for bn128 gadgets | Libraries that wrap 0x06 / 0x07 / 0x08 with consistent error handling reduce foot-guns. |
Compiler/EIP-Based Protections
- EIP-196 (Byzantium): Defines alt_bn128 precompiles including BN256MUL; canonical reference for intended curve and encoding behavior.
- EIP-1108 (Istanbul): Reprices BN256MUL gas to 6,000; dramatically lowers cost of honest verifiers but increases deployed surface area for curve arithmetic.
- Solidity / high-level patterns: Prefer abstractions that revert on failed
staticcallto0x07rather than raw assembly without checks; the compiler does not auto-mitigate T3 in hand-written Yul.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Notes |
|---|---|---|---|---|
| T1 | Smart Contract / Client | Critical | Low (patched clients) / High (during active bug window) | CVE-2025-30147 class; wrong curve math affecting verifiers and consensus. |
| T2 | Smart Contract / Infra | Low | Low | Data-dependent timing; minimal typical on-chain exploitability. |
| T3 | Smart Contract | Medium | Medium | Unchecked staticcall and return size; stale memory in pairing pipelines. |
| P1 | Protocol | N/A (design tradeoff) | N/A | EIP-1108 enabled zkSNARK scaling; increases density of verifier logic. |
| P2 | Protocol / Engineering | High | Medium | Library swaps need differential testing; CVE-2025-30147 precedent. |
| P3 | Protocol (economic) | Low | N/A | Fixed gas vs. variable work; node-load / scheduling margin, not soundness. |
Related Precompiles
| Precompile | Relationship |
|---|---|
| BN256ADD (0x06) | Point addition on the same curve; verifiers almost always combine add and mul before pairings. |
| BN256PAIRING (0x08) | Pairing check consumes G1/G2 points produced by upstream add/mul steps; errors in 0x07 break pairing soundness. |
| ECRECOVER (0x01) | Different curve (secp256k1), but same lesson: silent failure modes and caller-side checks dominate security. |
| MODEXP (0x05) | Used in RSA-style and modular arithmetic gadgets; often appears alongside heavy crypto in verification contracts. |