On October 24, Ori Pomerantz reported a bug affecting the use of verbatim builtin in Yul code. After investigating, the team was able to confirm the problem and locate its origin. The bug existed in the Block Deduplicator optimizer step, wherein equivalent assembly blocks are identified and merged. verbatim assembly items surrounded by identical opcodes were incorrectly considered identical and unified.
The bug existed since version 0.8.5, which introduced verbatim, and only affected pure Yul compilation with optimization enabled. Solidity code or Yul used in inline assembly blocks would not trigger it.
The use of verbatim is uncommon and the conditions which trigger the bug (two or more verbatim items surrounded by identical opcodes) are very specific. Also, to the extent of the investigations made by the team, there is no evidence that this could be used as an exploit or attack vector. While, if present, the impact of the bug is severe, its likelihood is very low. Considering that, the team assigned the bug an overall score of low.
Which Contracts Are Affected?
The conditions under which the bug might be triggered are as follows:
- Compilation of pure Yul code.
- Multiple calls to verbatim builtins with different data.
- Block Deduplicator optimizer step being in use.
Note that the Block Deduplicator is enabled by default when the optimizer is enabled.
If your project does not include contracts written purely in Yul or does not use verbatim, then there is no risk of it being impacted.
Technical Details
Yul code that uses verbatim such as the following example is prone to the bug:
verbatim.yul
{ let special := 0xFFFFFFFFFFFF let input := sload(0) let output switch input case 0x00 { output := verbatim_1i_1o(hex"506000", special) } case 0x01 { output := 1 } case 0x02 { output := verbatim_1i_1o(hex"506002", special) } case 0x03 { output := 3 } sstore(0, output) }
In this case, the block deduplicator incorrectly considered that the blocks of cases 0x00 and 0x02 are identical and thus could be merged into a single one.
The deduplication step is part of the opcode-based optimizer which operates on assembly code. It splits the sequence of instructions into blocks usually separated by JUMPS and JUMPDESTs. The blocks are analyzed and a series of optimization steps are performed. During the deduplication step, the optimizer maps which tags can be replaced by one another, because they refer to blocks of identical sequences of opcodes. The blocks are considered identical if both have the same opcodes in the same order.
The block deduplicator identifies blocks of assembly opcodes which are sequentially executed until reaching an opcode capable of altering the control flow, such as JUMP, RETURN or REVERT. The optimizer then checks if there are two blocks formed by identical sequences of assembly opcodes. In case such blocks exist, the optimizer replaces the label referring to one block by the label of the other, which means that later one block will be unused and can then be erased.
The bug manifested when checking two blocks for equality. The comparison for verbatim assembly items incorrectly considers all such items to be identical regardless of their data. So if verbatim instructions with different data appeared in blocks that are otherwise fully identical, they got deduplicated despite the difference in the verbatim data.
For example, when the Yul code snippet shown before is compiled without optimizations, it will include the assembly code fragment shown below:
solc --strict-assembly verbatim.yul --asm
tag_2: dup4 verbatimbytecode_506000 swap2 pop jump(tag_1) tag_3: 0x01 swap2 pop jump(tag_1) tag_4: dup4 verbatimbytecode_506002 swap2 pop jump(tag_1)
Notice that the blocks referred to by tag_2 and tag_4 are almost identical. As a result of the bug, the deduplication step of the optimizer would consider the verbatim items as equal despite their different contents. The optimizer would then incorrectly map tag_4 as a replacement for tag_2 which would further cause the block referred to by tag_2 to be erased later.
The result of the bug can be seen in the output of the optimized code below:
solc --strict-assembly verbatim.yul --asm --optimize
0x00 dup1 sload dup1 iszero tag_5 jumpi dup1 0x01 eq tag_3 jumpi dup1 0x02 eq tag_5 jumpi 0x03 eq tag_7 jumpi 0x00 sstore stop tag_7: pop 0x03 0x00 sstore stop tag_5: pop pop 0xaabbccddeeff verbatimbytecode_506002 0x00 sstore stop tag_3: pop pop 0x01 0x00 sstore stop
Note the two jumps to the block starting at tag_5. The block replaced both instances of the verbatimbytecode item, despite their content being different.