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).
runs: 1— optimize for minimum deployment cost, larger per-call costruns: 200— the default, balanced for typical contractsruns: 10000— optimize for cheap calls, more expensive deployment
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:
- 0.8.13 with the
abi.encodecalldata bug - 0.8.15 and 0.8.16 with the Yul memory copy bug
- 0.8.17 with the MCOPY edge case
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:
- Every developer on the team compiles with the same compiler
- CI/CD produces identical bytecode to local development
- Auditors know exactly what compiler was used
- 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:
- Satisfy all dependency minimum version requirements
- Have no known critical bugs
- 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:
- Identify the
pragma soliditydirective in every file - Check whether the pragma is floating or locked
- Cross-reference the locked version (or the range of a floating version) against the known bug list
- Flag any floating pragma as a finding (typically medium severity)
- 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.