← Back to Blog

Solidity Compiler Version Security: Known Bugs and Safe Version Practices

2026-04-18 solidity compiler pragma security compiler bugs version 2026

In 2022, a project compiled with Solidity 0.8.15 deployed a contract that used a specific Yul optimizer pattern involving memory copies. The optimizer generated incorrect code for that pattern — code that passed every unit test but silently corrupted memory under specific runtime conditions. The Solidity team confirmed the bug and issued 0.8.16 as a patch. Projects that had locked their pragma to 0.8.15 and never updated were shipping broken bytecode without knowing it.

Compiler bugs are different from Solidity logic bugs. You can audit the source code perfectly and still deploy vulnerable contracts if the compiler itself generates incorrect bytecode. This post covers which versions carry known critical bugs, how to check your toolchain, and why floating pragma makes the problem worse.


Why Compiler Version Matters

Every Solidity file starts with a pragma directive that tells the compiler which version to use:

pragma solidity ^0.8.0;   // floating — accepts any 0.8.x
pragma solidity 0.8.24;   // locked — only this exact version

These two lines look similar but have fundamentally different security properties. The floating version accepts dozens of compiler releases, including versions with known critical bugs. The locked version compiles with exactly one compiler, so its behavior is predictable and auditable.

Two factors make the compiler version a security surface:

1. Optimizer bugs. The Solidity compiler includes an optimizer that rewrites the generated bytecode to reduce gas costs. When the optimizer has a bug, it can produce bytecode that doesn't match what the source code says. These bugs are subtle — they only trigger for specific code patterns, which is why they pass most test suites.

2. Codegen bugs. Separately from the optimizer, the compiler's code generation logic has had bugs that produce incorrect bytecode even with the optimizer disabled. ABI encoding errors, memory layout bugs, and constructor initialization issues all fall into this category.


Notable Historical Bugs by Version

Before 0.4.22: Constructor Name Ambiguity

Before 0.4.22, constructors were defined as a function with the same name as the contract:

// Pre-0.4.22 style
contract Owned {
    address public owner;

    function Owned() public {
        owner = msg.sender;
    }
}

If the contract was renamed (e.g., from Owned to OwnedV2) but the constructor function name was not updated, the constructor became a public function callable by anyone. The "constructor" could be called post-deployment to reset owner to the attacker.

Solidity 0.4.22 introduced constructor() syntax to eliminate this ambiguity. Any codebase still using function-name constructors carries this risk if the contract is ever refactored.

Before 0.5.0: Missing callvalue() Check in Constructors

Constructors in certain patterns before 0.5.0 did not correctly insert the callvalue() check that prevents ETH from being sent to non-payable constructors. This could allow ETH to be sent during deployment to a contract not designed to receive it, with no way to recover the funds afterward.

0.5.0 also enforced explicit view and pure state mutability declarations. Prior versions allowed functions that modified state to be declared view, with no compiler enforcement — a source of many subtle state mutation bugs that slipped through.

Before 0.6.0: ABIEncoderV2 and Storage Array Copy

ABIEncoderV2 was experimental from 0.4.x through 0.5.x, enabled via:

pragma experimental ABIEncoderV2;

The experimental encoder had several encoding bugs for nested structs and dynamic types. More critically, versions before 0.6.0 contained a storage array copy bug: under specific conditions, copying a storage array to another storage location could leave stale data in the destination. An attacker who knew the layout could read or write storage slots that should have been overwritten.

This bug was severe enough to receive formal CVE tracking. It affects any pre-0.6.0 contract that copies storage arrays between storage variables.

0.8.13: abi.encode with Nested Calldata Arrays

Solidity 0.8.13 introduced a bug in abi.encode calls where the first argument was a nested dynamic type from calldata. The encoded output had incorrect offsets, meaning contracts that encoded calldata arrays for cross-contract calls could produce malformed ABI data. Any contract that passed this malformed data to another contract for decoding would behave incorrectly.

// Pattern that triggered the 0.8.13 bug
function relay(bytes[] calldata data) external {
    bytes memory encoded = abi.encode(data);  // incorrect encoding in 0.8.13
    target.call(encoded);
}

The fix shipped in 0.8.14.

0.8.15 and 0.8.16: Yul Optimizer Memory Copy Bug

This is the bug from the opening example. The Yul optimizer in 0.8.15 and 0.8.16 generated incorrect code for certain mstore/mload patterns that appeared in memory copy sequences. When the optimizer inlined and reordered these operations, it could produce bytecode where the destination memory was not correctly populated.

The pattern most commonly triggered when:
- A function returned a memory struct containing dynamic arrays
- The optimizer decided to inline the memory copy
- The inlining incorrectly reordered the write operations

Contracts that called such functions would receive corrupted return data. 0.8.16 partially fixed this; 0.8.17 completed the fix.

0.8.17: MCOPY Opcode Edge Cases

The MCOPY opcode (EIP-5656) was introduced for more efficient memory copies. Solidity 0.8.17's code generation used MCOPY in specific patterns but had edge cases where overlapping source and destination ranges produced incorrect results. This only affected deployments on EVM versions that supported MCOPY (Shanghai and later), but contracts compiled with 0.8.17 targeting those EVM versions carried the risk.


The Optimizer: Runs Value and Security Implications

The optimizer is enabled in most production configs:

// hardhat.config.js
solidity: {
  version: "0.8.24",
  settings: {
    optimizer: {
      enabled: true,
      runs: 200
    }
  }
}

The runs value is frequently misunderstood. It is not the number of optimization passes — it is an estimate of how many times each function will be called over the contract's lifetime. The optimizer uses this to decide whether to optimize for deployment cost (low runs) or call cost (high runs).

For DeFi protocols where core functions are called thousands of times, runs: 1000 or higher reduces per-call gas meaningfully. For contracts deployed once and called rarely, runs: 200 or lower keeps deployment costs down.

Security implication: Disabling the optimizer means optimizer-specific bugs cannot affect your bytecode — for the 0.8.15/0.8.16 Yul bug, the incorrect codegen only triggered with the optimizer enabled. But disabling optimization increases gas costs significantly. The practical approach is to use a compiler version without known optimizer bugs rather than disabling optimization entirely.


Floating Pragma: Why ^0.8.0 Is a Security Risk

// VULNERABLE: floating pragma
pragma solidity ^0.8.0;

The caret operator means "this version or any compatible higher version." ^0.8.0 accepts every compiler from 0.8.0 through 0.8.29 (the highest 0.8.x release at time of writing). That range includes:

A developer who runs npm install on a project using ^0.8.0 may get a different compiler version than the original author used. The contract compiles cleanly, passes the same tests, and produces different bytecode — potentially incorrect bytecode.

This is especially dangerous for library authors. If you publish a library with ^0.8.0, every downstream project that uses your library through a floating pragma inherits the ability to compile it with a buggy compiler.


Locked Pragma: The Correct Approach

// SAFE: locked pragma
pragma solidity 0.8.24;

No caret, no tilde. One version. The benefits:

  1. Every developer on the team compiles with the same compiler
  2. CI/CD produces identical bytecode to local development
  3. Auditors know exactly what compiler was used
  4. You can verify the compiler version has no known critical bugs before committing to it

Which version to lock to? As of April 2026, 0.8.24 is a stable release with no known critical bugs. It includes fixes for all the bugs described above and has had sufficient time in production for issues to surface. Check the official Solidity bug list before committing to any version — the list is maintained by the Solidity team and includes severity ratings.


How to Check Your Version for Known Bugs

Method 1: Solidity documentation bug list

The Solidity team maintains a machine-readable JSON list of known bugs at:

https://github.com/ethereum/solidity/blob/develop/docs/bugs.json

Each entry includes the affected version range, severity (low/medium/high), and a description. Cross-reference your compiler version against this list before deployment.

Method 2: solc --version and --help

solc --version
# solc, the solidity compiler commandline interface
# Version: 0.8.24+commit.e11b9ed9.Linux.g++

solc --help | grep -i optimizer

Method 3: solc-select

solc-select lets you install and switch between multiple Solidity compiler versions locally:

# Install solc-select
pip install solc-select

# Install specific versions
solc-select install 0.8.24
solc-select install 0.8.15

# Switch the active compiler
solc-select use 0.8.24

# Verify
solc --version

This is essential for projects that need to compile contracts originally written for older versions, or for auditors who need to reproduce exactly what was deployed. Hardhat and Foundry both allow you to specify the compiler version in config, but solc-select controls the system-level compiler for projects that invoke solc directly.


Library and Dependency Conflicts

OpenZeppelin contracts specify minimum Solidity version requirements in their package.json:

{
  "peerDependencies": {
    "solidity": ">=0.8.20 <0.9.0"
  }
}

If your locked pragma is below the minimum required by your dependencies, compilation will fail. If it is within the acceptable range but targets a version with known bugs, the dependency will compile — it just may compile incorrectly.

Check node_modules/@openzeppelin/contracts/package.json and any other dependency's peerDependencies before finalizing your locked version. The version you choose must:

  1. Satisfy all dependency minimum version requirements
  2. Have no known critical bugs
  3. Be supported by your deployment toolchain (Hardhat, Foundry, etc.)

When there is a conflict between a library's minimum version and a safe compiler version, prefer upgrading the library to one compatible with the safe compiler over downgrading to a buggy compiler to satisfy old peerDependencies.


Compiler Version in Audit Scope

Every security audit begins with a compiler version check. Auditors will:

  1. Identify the pragma solidity directive in every file
  2. Check whether the pragma is floating or locked
  3. Cross-reference the locked version (or the range of a floating version) against the known bug list
  4. Flag any floating pragma as a finding (typically medium severity)
  5. Flag any version with known critical bugs as high severity

A floating pragma in a production contract is a guaranteed audit finding. Even if the current compiler you are using has no known bugs, the pragma allows a buggy compiler to produce the bytecode — and that possibility is what auditors are required to document.


What ContractScan Detects

Issue Detection Severity
Floating pragma (^, >=, >) Static analysis Medium
Version below 0.8.0 (no overflow protection) Static analysis High
Version in known-bug range (0.8.13, 0.8.15, 0.8.16, 0.8.17) Version check High
Optimizer enabled on a buggy version Combined check High
No pragma statement Static analysis High
Mismatched pragma across files in same project Static analysis Medium
Version incompatible with detected OpenZeppelin import Dependency check Medium

ContractScan checks the pragma statement, validates the version against the known bug list, and flags optimizer settings that interact with known optimizer bugs. If your contract was compiled with 0.8.15 and the optimizer enabled, the report will call that out specifically.


Check your compiler version risks at https://contract-scanner.raccoonworld.xyz


Related: Solidity Security Best Practices 2026 — a broader reference for writing secure Solidity contracts from the ground up.

Related: How to Use Slither for Smart Contract Security — running static analysis locally and integrating it into your CI pipeline.

Important Notes

This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.

Scan your contract for this vulnerability
Free QuickScan — Unlimited quick scans. No signup required.. No signup required.
Scan a Contract →