We are excited to announce the release of the Solidity Compiler v0.8.26. This newest version of the compiler brings support for custom errors in require, improved default Yul Optimizer sequence that will speed up compilation via IR, several bugfixes, and more!
Notable Features
Custom errors support in require
Custom errors in Solidity provide a convenient and gas-efficient way to explain to the user why an operation failed. Solidity 0.8.26 introduces a highly anticipated feature that enables the usage of errors with require function.
The require function in pre 0.8.26 versions provided two overloads:
- require(bool) which will revert without any data (not even an error selector).
- require(bool, string) which will revert with Error(string).
In this release we are introducing a new overload to support custom errors:
- require(bool, error) which will revert with the custom, user supplied error provided as the second argument.
Let's look at an example to understand the usage of the require function with custom errors:
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.26; /// Insufficient balance for transfer. Needed `required` but only /// `available` available. /// @param available balance available. /// @param required requested amount to transfer. error InsufficientBalance(uint256 available, uint256 required); // This will only compile via IR contract TestToken { mapping(address => uint) balance; function transferWithRequireError(address to, uint256 amount) public { require( balance[msg.sender] >= amount, InsufficientBalance(balance[msg.sender], amount) ); balance[msg.sender] -= amount; balance[to] += amount; } // ... }
Note that, just like in the previously available overloads of require, arguments are evaluated unconditionally, so take special care to make sure that they are not expressions with unexpected side-effects. For example, in require(condition, CustomError(f())) and require(condition, f()), the call to function f() will always be executed, regardless of whether the supplied condition is true or false.
Note that currently, using custom errors with require is only supported by the IR pipeline, i.e. compilation via Yul. For the legacy pipeline, please use the if (!condition) revert CustomError(); pattern instead.
Optimization for reverts with errors of small static encoding size
In cases with custom errors of small static encoding size, for example, an error without parameters, or parameters small enough that they could fit into scratch space, developers often resorted to performing such reverts in inline assembly in order to save on deployment gas cost.
As of this release, a check is performed at the code generation stage, and said optimization applied if possible, which means that the following case is now as optimal as the inline assembly variant:
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.26; error ForceFailure(); contract FailureForcer { function fail() external pure { revert ForceFailure(); } }
New, faster Yul Optimizer sequence
One of the highlights of this release is the improved default sequence used by the Yul Optimizer.
A sequence tells the optimizer module which steps to run and in which order. It can be supplied by the user, but the recommended default is hard-coded in the compiler, since crafting a good sequence is a non-trivial task. The choice of sequence mostly affects the code generated by the IR pipeline, but it also has a small effect on the legacy pipeline, since inline assembly and utility code generated by the compiler are both optimized this way.
As a part of the ongoing effort to improve performance of the new pipeline, we analyzed the current default sequence to determine which parts are contributing the most to the final result.
A major feature of the old sequence was its main loop - the long middle segment that could be repeated, giving the optimizer a chance to improve the result if the previous pass created new optimization opportunities. Our analysis showed, however, that the result of the first pass is almost always very close to the final result and subsequent repetitions of the main loop contribute only a little. While simple removal of the loop gives results that are still noticeably worse than with the old sequence, with some experimentation we managed to create a new sequence that provides comparable optimization quality in a single pass.
For example, this is how bytecode size after each optimization step in the current sequence looks like for some of the sample contracts we analyzed:
The new sequence stops much earlier:
Similarly for runtime gas. Current sequence:
New sequence:
The table below shows the effect of the new sequence on compilation time and bytecode size in several real-life projects that we use for benchmarking:
Project | Compilation Time1 | Bytecode Size | Runtime Gas |
---|---|---|---|
pool-together | -63% | -1.29% | |
uniswap | -53% | +1.67% | |
zeppelin | -47% | -0.48% | -0.01% |
elementfi | -42% | -1.87% | |
euler | -34% | +1.00% | |
yield_liquidator | -27% | +0.84% | +0.14% |
ens | -22% | -1.20% | -0.01% |
brink | -20% | +0.61% | |
perpetual-pools | -16% | -0.23% | +0.02% |
gp2 | -12% | +0.50% |
While we don't have runtime gas results for all projects listed above, due to issues with executing their test suites, in the ones we do have the differences are quite small.
Based on our benchmarks we expect up to a 65% decrease in compilation time via IR in most projects. While the effect on bytecode size is not always positive, the differences are generally small enough to be worth the improved compilation time. We expect upcoming improvements to the optimizer to have effects much bigger than that.
If you observe significantly degraded optimization quality in your project, we recommend temporarily switching back to the old sequence and opening an issue so that we can investigate. The default sequence in Solidity v0.8.25 included the following steps:
dhfoDgvulfnTUtnIf [ xa[r]EscLM cCTUtTOntnfDIul Lcul Vcul [j] Tpeul xa[rul] xa[r]cL gvif CTUca[r]LSsTFOtfDnca[r]Iulc ] jmul[jul] VcTOcul jmul : fDnTOcmu
Warning: We make utmost efforts to ensure that the compiler works correctly regardless of the sequence used, employing fuzz testing to find any abnormalities, but by its very nature the default sequence is receiving a lot more coverage and problems with custom sequences are much more likely to remain undetected. For this reason, while the new sequence can also be used with older compilers, we recommend extreme care while doing so. In particular, the new sequence is susceptible to the FullInliner Non-Expression-Split Argument Evaluation Order Bug, which is not an issue for the recent versions, but would cause problems on versions older than v0.8.21.
Replacement of the internal JSON library
In this release we also replaced our internal JSON library jsoncpp with nlohmann::json.
Because of that, the formatting of the JSON output slightly changed, where it also became more strict with UTF-8 encodings. The old jsoncpp allowed some invalid UTF-8 sequences, but also did not handle them properly.
However, we don't expect it to create problems in practice because the vast majority of implementations assume UTF-8 anyway.
Full Changelog
Language Features
- Introduce a new overload require(bool, Error) that allows usage of require functions with custom errors. This feature is available in the via-ir pipeline only.
Compiler Features
- SMTChecker: Create balance check verification target for CHC engine.
- Yul IR Code Generation: Cheaper code for reverting with errors of a small static encoding size.
- Yul Optimizer: New, faster default optimizer step sequence.
Bugfixes
- Commandline Interface: Fix ICE when the optimizer is disabled and an empty/blank string is used for --yul-optimizations sequence.
- Optimizer: Fix optimizer executing each repeating part of the step sequence at least twice, even if the code size already became stable after the first iteration.
- SMTChecker: Fix false positive when comparing hashes of same array or string literals.
- SMTChecker: Fix internal error on mapping access caused by too strong requirements on sort compatibility of the index and mapping domain.
- SMTChecker: Fix internal error when using an empty tuple in a conditional operator.
- SMTChecker: Fix internal error when using bitwise operators with an array element as argument.
- Standard JSON Interface: Fix ICE when the optimizer is disabled and an empty/blank string is used for optimizerSteps sequence.
- StaticAnalyzer: Only raise a compile time error for division and modulo by zero when it's between literals.
- Yul Optimizer: Fix the order of assignments generated by SSATransform being dependent on AST IDs, sometimes resulting in different (but equivalent) bytecode when unrelated files were added to the compilation pipeline.
Build System
- Replace internal JSON library jsoncpp with nlohmann::json.
How to Install/Upgrade?
To upgrade to the latest version of the Solidity Compiler, please follow the installation instructions available in our documentation. You can download the new version of Solidity here: v0.8.26. If you want to build from the source code, do not use the source archives generated automatically by GitHub.
Footnotes
-
Note that the numbers in the table refer to the total compilation time, which includes analysis, code generation, optimization and especially Yul->EVM transform, while the diagrams shown earlier only include the time spent executing the sequence. ↩