Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
Comment thread
nikola-matic marked this conversation as resolved.
date: '2026-05-20'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Comment thread
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the params are (a_offset, a_length, b) and the call site yields a permutation (42, s_offset, s_length), so the offset becomes 42, the length becomes the offset, and the value of b is the length, isn't it? So it would be that the offset field is read from a value type slot, not 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.