Misordered named parameters bug blog post - #205
Conversation
| layout: post | ||
| published: true | ||
| title: 'Misordered Named Parameters in require with Custom Errors Bug' | ||
| date: '2026-05-20' |
| category: Security Alerts | ||
| --- | ||
|
|
||
| On February 9, 2026, the Solidity team discovered a bug in the IR-based code generator that |
There was a problem hiding this comment.
Do we know the actual reporter (aside from the EF bug bounty), and should we use them instead of "Solidity team"?
There was a problem hiding this comment.
It was people from Spearbit. Hari will know.
Or you can ping EF - we still did not notify them that we changed the severity of this after our initial response and did not send impact analysis.
czepluch
left a comment
There was a problem hiding this comment.
looks good. mostly cosmetic nits from my side.
6275181 to
a708add
Compare
✅ Deploy Preview for solidity-website ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
cameel
left a comment
There was a problem hiding this comment.
Just wording and minor inaccuracies. The overall structure is fine.
|
|
||
| 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), released on May 20, 2026, provides a fix. |
There was a problem hiding this comment.
This date needs to be updated too, but I'd actually just omit it. Specifying version is unambiguous enough.
|
|
||
| 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 contract state is not modified by it. |
There was a problem hiding this comment.
| to revert, so contract state is not modified by it. | |
| to revert, so it has no impact on contract's state. |
|
|
||
| ## Technical Details | ||
|
|
||
| A custom error declaration fixes the order of its parameters, and that order determines its |
There was a problem hiding this comment.
| A custom error declaration fixes the order of its parameters, and that order determines its | |
| A custom error declaration specifies the order of its parameters, and that order determines its |
| Solidity also allows the caller to pass arguments by name, optionally in a different order | ||
| from the declaration: |
There was a problem hiding this comment.
"also" here sounds weird, as if you just showed the version with positional arguments, but you did not.
| Solidity also allows the caller to pass arguments by name, optionally in a different order | |
| from the declaration: | |
| Solidity allows the caller to pass arguments by name, optionally in a different order | |
| from the declaration: |
| by any reordering. Most surprisingly, this includes reorderings of two arguments of the same | ||
| type, where no positional type mismatch could ever surface. |
There was a problem hiding this comment.
Why is that surprising? To me it's actually the more obvious case. Lack of type mismatch means that the compiler or decoder can't detect it so no compilation or runtime error can save you. It's the fact that the compiler won't fail trying to encode mismatched types that was more surprising to me.
| reordered with respect to a value-type argument, the slots passed to the encoding helper are | ||
| permuted at the level of *stack slots*, not at the level of named parameters, and the helper | ||
| interprets its inputs against parameter positions of a different width than intended. |
There was a problem hiding this comment.
They are not permuted as individual stack slots. They're still grouped. Just in the wrong way.
Also, "parameter positions of a different width" sounds weird to me. I know what you mean, but only because I already know how the bug works. Generally, referring to it as "width" or "stack width" sounds wrong.
| field is therefore read from a value-type slot, and the string data is copied from an | ||
| arbitrary calldata offset. |
There was a problem hiding this comment.
| field is therefore read from a value-type slot, and the string data is copied from an | |
| arbitrary calldata offset. | |
| field is therefore read from a value-type slot, and the string data is copied from a | |
| random calldata offset. |
| In the most severe configurations the encoding aborts 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. In less severe configurations the payload is well-formed |
There was a problem hiding this comment.
I'd say that the latter is worse, because it's sometimes possible for the receiver to successfully decode it.
Actually, the current text makes it sound like the data can never be decoded. Often it actually can. It's just numbers. A small integer can easily pass for a valid offset.
| reorders arguments before encoding, and so are not affected. The bug is specific to the | ||
| `require`-with-custom-error path introduced in 0.8.26. | ||
|
|
||
| ### Misalignment With Mixed Stack Widths |
There was a problem hiding this comment.
You mention two cases in the post:
- swapping parameters of the same type
- swapping parameters of different types represented using different numbers of stack slots
The third possibility is missing: different types with same numbers of stack slots. It's also not considered in the impact section.
| ## Impact | ||
|
|
||
| Because the bug affects only the construction of revert data for transactions that are | ||
| already going to revert, it cannot be used to manipulate storage, return values, or external | ||
| calls. Its primary consequence is misleading off-chain consumers - block explorers, indexers, | ||
| error-decoding libraries, and test frameworks - that decode the payload as if the values had | ||
| been placed correctly. In the stack-misalignment cases the failure is more visible: an | ||
| encoding-level revert can replace the intended custom-error revert, so off-chain code that | ||
| distinguishes specific custom errors from generic reverts may take a different branch than | ||
| expected. | ||
|
|
||
| Named-argument syntax is uncommon in `require` calls in practice, and the bug remained | ||
| undetected for almost two years. The narrow trigger conditions and the read-only nature of the | ||
| affected code path together place this bug in the "very low" severity tier. |
There was a problem hiding this comment.
- It can have on-chain effects - reverts can be intercepted with try/catch or by using a bare call. It's just that this is not (and should not be) commonly done.
- Do we want to also mention the ERC that made us reconsider the severity of this?
66d46e8 to
04ca6ff
Compare
04ca6ff to
2a1511e
Compare
clonker
left a comment
There was a problem hiding this comment.
Some small comments. Should we mention that the revert always happens? (it does, doesn't it? just with weird data).
|
|
||
| ## Which Contracts Are Affected? | ||
|
|
||
| A contract is affected when **all** of the following conditions hold: |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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.
It does say that already, in a way:
I wanted to suggest that too at first but in the end decided that the above are already enough. But if you also thought that, maybe there's something to it and we should make that more explicit. |
Date TBD