-
Notifications
You must be signed in to change notification settings - Fork 28
Misordered named parameters bug blog post #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| --- | ||
| layout: post | ||
| published: true | ||
| title: 'Misordered Named Parameters in require with Custom Errors Bug' | ||
| date: '2026-05-20' | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't forget |
||
| 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. string/bytes literals are inserted directly at declaration order, so these are unaffected as far as i can tell - then you'd need at least two non-string/byte lit named args |
||
|
|
||
| 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. | ||
|
|
||
|
cameel marked this conversation as resolved.
|
||
| ## 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the params are |
||
| 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. | ||
Uh oh!
There was an error while loading. Please reload this page.