Case StudyJune 2026 · Bytecode Assurance · solc

Detecting a compromised solc compiler

By Meridion Risk · June 2026

Almost every assurance signal in the EVM ecosystem terminates at a single tool. Smart contract security audits reason about source code, and verification services such as Etherscan confirm that published source recompiles to the deployed bytecode. Both steps implicitly trust the Solidity compiler one hundred percent. If solc is honest, the chain of trust holds. If solc is not, every downstream control inherits the compromise.

The Solidity compiler is one of the highest-value targets in Web3. A single malicious build that survives in a release channel, a CI cache, or a pinned dependency can silently inject backdoors into thousands of contracts that each pass source audit and source verification.

The mandate

In this engagement we were asked one question. Is it possible to detect a compromised version of solc, a compiler that quietly injects backdoors into otherwise-correct source, from the deployed artifact alone? The method we built takes three inputs the client already has on hand: the source code, the deployed runtime bytecode, and the project's test suite. From these it produces a clear attestation as to whether the bytecode contains reachable behavior the source never authorized.

The reason this matters is structural. Source verification only proves that the published source recompiles to the bytecode under the stated settings. It says nothing about behavior the source author never wrote. A compromised compiler produces malicious bytecode that recompiles cleanly, passes review, and verifies on-chain, so every control built around source code security is satisfied while the backdoor ships.

Simulating a compromised compiler

We did not have a real backdoored solc to test against, so we built one by hand. Rather than inject arbitrary noise, we modelled the two backdoor classes a rational attacker is most likely to choose. Both maximize payoff while keeping the odds of being caught at the source layer low:

Our method

The analysis works directly on the deployed runtime bytecode and proceeds in three steps.

Example 1: an unintended function selector that drains the contract

The declared contract exposed a normal ERC-20-style interface. Entrycode mapping, however, recovered one dispatch edge with no counterpart in the ABI: a comparison against the constant selector 0x6b8ff574. No source function hashed to that selector, and no audit artifact referenced it. The path it guards is short and unmistakable:

// Reconstructed from runtime bytecode. Absent from source and ABI.
if (msg.sig == 0x6b8ff574) {
    // unconditional: no caller check, no value check
    IERC20(address(this)).transfer(
        0x00000000000000000000000000000000DEADBeef,  // hardcoded sink
        IERC20(address(this)).balanceOf(address(this))
    );
    return;
}

The selector is callable by anyone. Because it never appears in the ABI, source review and Etherscan's source view give no indication it exists, and only an exhaustive map of the dispatch table surfaces it. Concolic execution and SMT solving then proved the entrypoint is reachable and is not accounted for by any path the test suite exercises. This is a formal result, not a hunch: an entrypoint with no source-of-truth origin, no access guard, and a hardcoded recipient. That is a complete unauthorized drain path injected below the source layer.

Example 2: a malicious branch condition inside an authorized function

The second backdoor is subtler because it hides inside a function that genuinely exists. The source declares a privileged withdraw guarded by a standard owner check. Exploring the paths the test suite drives recovered the deployed guard and found it admits a second, constant operand the source never mentions:

// Source as written and audited
require(msg.sender == owner, "not owner");

// Guard actually emitted into the deployed bytecode
require(
    msg.sender == owner ||
    msg.sender == 0xA11CE00000000000000000000000000000000bad,  // hardcoded
    "not owner"
);

Every legitimate path still works exactly as specified, so functional tests, intended-operation fork tests, and source review all pass. But the authorized function now also authorizes one hardcoded attacker address. Here the entrypoint itself is legitimate, so the symbolic branch expression for the guard was handed to the LLM-based agent, which scored the extra || operand against the source and flagged the embedded constant address as having no source-level justification. This is an access-control bypass grafted onto a real function rather than a new function added.

Backdoor classWhere it hidesWhy source controls miss itWhat surfaced it
Unintended selector drainAn extra dispatch edge with no ABI entrySelector never appears in source, ABI, or Etherscan source viewEntrycode mapping, with concolic execution and SMT solving proving the path is unaccounted for
Malicious branch conditionInside an existing, legitimately authorized functionIntended behavior is unchanged; functional tests and source review passLLM-based legitimacy scoring of the symbolic branch expression against the source

What the engagement demonstrated

Both backdoor classes slip straight past the controls that secure source code, and both are detectable from deployed bytecode alone. The decisive shift is to stop trusting the compiler as an axiom and instead treat the bytecode as the thing under assessment: enumerate every real entrypoint, prove the surface is closed against the paths legitimate use actually drives, and score each remaining guard against what a faithful compilation should have produced. A compromised solc can lie about the source, but it cannot hide the callable paths it actually emitted.

The outcome is a new piece of pre-deployment assurance the client did not have before. Because we drive the analysis from recorded test traces rather than exploring the program's paths blindly, we sidestep the path-explosion problem that makes most symbolic execution too slow to run as a matter of routine. That keeps the cost low enough to run on every compilation, not just once before launch. Pointed at the bytecode and the test suite ahead of each deploy, it gives a defensible, repeatable answer to the one question source audits and verification cannot reach: does the artifact about to go on-chain contain any path the source never authorized?

For teams building on EVM, whose assurance ultimately rests on the compiler, this is the gap worth closing. If you want to know whether your deployed bytecode contains paths your source never authorized, submit a request.

Worried about what your compiler actually emitted?

We assess deployed bytecode for unintended entrypoints and injected branch conditions, independent of source and verification claims.

Submit a request