diff --git a/src/posts/2026-05-20-misordered-named-parameters-in-require-with-custom-errors-bug.md b/src/posts/2026-05-20-misordered-named-parameters-in-require-with-custom-errors-bug.md new file mode 100644 index 00000000..adc6b7b0 --- /dev/null +++ b/src/posts/2026-05-20-misordered-named-parameters-in-require-with-custom-errors-bug.md @@ -0,0 +1,167 @@ +--- +layout: post +published: true +title: 'Misordered Named Parameters in require with Custom Errors Bug' +date: '2026-05-20' +author: Solidity Team +category: Security Alerts +--- + +On February 4, 2026, a bug in the IR-based code generator was reported by Carl from +[Spearbit](https://spearbit.com/) through the Ethereum Foundation bug bounty program. The +bug causes the arguments of a custom error passed to `require` using named-parameter syntax +to be ABI-encoded in call-site order rather than declaration order. + +The bug was introduced in Solidity 0.8.26, which added support for passing custom errors as +the second argument to `require`. +[Solidity 0.8.37](https://github.com/ethereum/solidity/releases/tag/v0.8.37) provides a fix. +Due to the very low severity of the bug, described below, the fix was not prioritized for an +expedited release. + +We assigned the bug a severity of "very low". +The affected code is the construction of revert data for a transaction that is already going +to revert, so it has no impact on contract's state. +Triggering the bug additionally requires a fairly specific pattern - a custom error passed to +`require` and instantiated with named arguments in a non-declaration order - and only affects +the IR-based pipeline. + +## Which Contracts Are Affected? + +A contract is affected when **all** of the following conditions hold: + +1. The contract is compiled with the IR-based pipeline (`--via-ir` on the command line or + `viaIR: true` in Standard JSON). The evmasm pipeline is unaffected. +2. The contract uses `require(condition, ErrorName({...}))` where `ErrorName` is a user-defined + error. +3. The named arguments are written in an order that differs from the order declared by the + error definition. Positional arguments, or named arguments written in declaration order, + produce the correct encoding. + +Other language constructs that accept named arguments are **not** affected. As part of the due +diligence for the fix, all of them were checked and covered with tests: + +- `revert ErrorName({...})` statements +- event emissions (`emit E({...})`) +- internal, external, library, bound, and virtual function calls, including calls via function pointers +- getters +- struct constructor invocations +- inherited constructors +- modifier invocations +- type conversions + +Until upgrading to a fixed version, the bug can be avoided by writing the named arguments in +declaration order, or by using positional arguments. + +## Technical Details + +A custom error declaration specifies the order of its parameters, and that order determines its +ABI signature and the layout of its encoded payload: + +```solidity +error NamedArgsError(uint256 a, uint256 b); +``` + +Solidity allows the caller to pass arguments by name, optionally in a different order +from the declaration: + +```solidity +require(false, NamedArgsError({b: 7, a: 2})); +``` + +The compiler binds named arguments to parameters *by name*, so any permutation that mentions +every parameter exactly once is accepted by the type checker. The type system does not +require the call-site order to match the declaration. As a result, the bug could be triggered +by any reordering. This includes swaps of two arguments of the same type - a case where +nothing can catch the mistake, since there is no type mismatch for the compiler or a decoder +to detect. Perhaps more surprisingly, the compiler would also silently encode reorderings of +arguments of *different* types, without any error during compilation. + +In the IR-based code generator, the lowering of `require` with a custom error passed the +arguments to the encoding helper in the order they appeared at the call site, instead of +first reordering them to match the parameter order of the error. With the snippet above, the +contract reverted with a payload whose first word held the value of `b` (`7`) and whose +second word held the value of `a` (`2`), so off-chain decoders would read `a = 7, b = 2` +instead of the intended `a = 2, b = 7`. + +Standalone `revert ErrorName({...})` statements go through a different code path that +reorders arguments before encoding, and so were not affected. The bug was specific to the +`require`-with-custom-error path introduced in 0.8.26. + +Reordering parameters of *different* types that occupy the same number of stack slots +(say, a `uint256` and an `address`) also produces no misalignment: each value simply lands +in the other parameter's position and is encoded as if it had the other type. The payload +stays structurally valid, but decoders reinterpret the values under the wrong types. + +### Misalignment With Multi-Slot Arguments + +The consequences extend beyond a swap of word-sized values when the arguments occupy +different numbers of stack slots. References to arrays and array slices +in `calldata` are represented by *two* stack slots (offset and length), whereas most value types +and references in other data locations use a single slot. When a `calldata` reference is +reordered with respect to a value-type argument, each argument's slots still arrive at the +encoding helper grouped together, but the groups come in call-site order while the helper +partitions the slots it receives according to the declaration order. The boundaries between +arguments no longer line up, and a parameter can be handed slots that belong to a different +argument. + +Consider: + +```solidity +error StringAndUint(string a, uint256 b); + +contract C { + function f(string calldata s) external pure { + require(false, StringAndUint({b: 42, a: s})); + } +} +``` + +The encoding helper for `StringAndUint` expects to first receive the two slots that describe +`a` (the calldata string's offset and length), followed by the single slot that holds `b`. +With the bug it receives `b`'s single slot first, followed by `a`'s two slots, and then +encodes them assuming the first two slots belong to the dynamic-type parameter. The length +field is therefore read from a value-type slot, and the string data is copied from a +random calldata offset. + +Depending on the actual values, the encoding can abort with an EVM-level error, which still +causes the transaction to revert - but with no decodable error data - rather than producing +the intended custom-error revert. Alternatively, the contract reverts with a payload that +does not match the error's ABI signature but may still decode successfully: the misplaced +words are just numbers, and a small integer can easily pass for a valid offset. This case is +arguably worse - a consumer that matches on the error selector and decodes the fields +observes plausible-looking values that have no relation to the source-level arguments. + +## Impact + +Because the bug affects only the construction of revert data for transactions that are +already going to revert, it does not modify storage, return values, or external calls in the +reverting transaction itself. Revert data can, however, be observed on-chain: a caller may +intercept it with `try`/`catch` or a bare `call` and branch on its contents, although doing +so is uncommon. The primary consequence is misleading consumers of revert data - block +explorers, indexers, error-decoding libraries, and test frameworks - that decode the payload +as if the values had been placed correctly, observing swapped or type-confused values. In +the stack-misalignment cases the failure is more visible: an encoding-level revert can +replace the intended custom-error revert, so code that distinguishes specific custom errors +from generic reverts may take a different branch than expected. + +Revert data can also be load-bearing: +[ERC-3668 (CCIP Read)](https://eips.ethereum.org/EIPS/eip-3668) relies on clients decoding +the `OffchainLookup` custom error to drive off-chain data retrieval. While ERC-3668 includes +mechanisms that protect against forged revert errors, they do not help when an honest +contract emits mis-encoded revert data due to a compiler bug. Since `OffchainLookup` mixes +dynamic and value types, a misordering in an affected `require` would most likely produce a +garbled payload that breaks the lookup flow, though not necessarily in an obviously detectable +way. Standards like this, which make revert data part of a contract's interface, factored +into our severity assessment. + +There is no meaningful avenue for third-party exploitation. An attacker cannot introduce the +mis-encoding into a correctly written contract - the misordered named arguments must already +be present in the contract's own source code - and an attacker who merely wants to present +misleading revert data to a consumer can already do so trivially by deploying a contract that +reverts with arbitrary bytes. The harm is therefore limited to contracts unknowingly +emitting wrong data to consumers that trust them. + +Named-argument syntax is uncommon in `require` calls in practice, and the bug remained +undetected for almost two years. The narrow trigger conditions, the read-only nature of the +affected code path, and the lack of any exploitation avenue together place this bug in the +"very low" severity tier.