Change in 0.8.0 Solidity compiler you might have missed

Anton Permenev Smart Contract Assurance, PwC Switzerland 28 Jan 2021

While experimenting with new built in safe math feature of the new Solidity compiler version 0.8.0, we have noticed some unexpected behavior. Here is a small snippet with a function that uses Solidity inline assembly feature.

   function asm_div(uint128 a) public {
        require(a == 100, "CHECK_1");
        uint128 y;
            y := div(a, 2)
        require(y == 50, "CHECK_2");

There are two user defined reverts this function can fail with:

1. Revert with "CHECK_1" message.
2. Revert with "CHECK_2" message.

We compiled this code with solc versions 0.8.0, 0.7.4, 0.6.12, 0.5.17 and 0.4.26 and tried to see which of those reverts are feasible. As it turned out, in versions < 0.8.0 both reverts can be reached. The surprise is that in 0.8.0 only the first revert is reachable.

Revert with "CHECK_1" as a message is trivially achievable by passing e.g 5 to the asm_div function in any compiler version. To reach the "CHECK_2" in <0.8.0 solc versions, one must remember one important warning in the Solidity documentation:

If you access variables of a type that spans less than 256 bits (for example uint64, address, or bytes16), you cannot make any assumptions about bits not part of the encoding of the type. Especially, do not assume them to be zero. To be safe, always clear the data properly before you use it in a context where this is important: uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ } To clean signed types, you can use the signextend opcode: assembly { signextend(<num_bytes_of_x_minus_one>, x) }

When a is 100 + 2^128 (magic value), the revert with "CHECK_1" as a message will be triggered, because only 16 low bytes of a will be checked. The division will be performed on full 32 bytes, that will yield 50 + 2^127. This value will violate the "CHECK_2".

Why can’t the 0.8.0 compiler achieve the same result? If the same magic value is passed for code compiled with this compiler version, the function still reverts, but earlier than the underflowing division. In addition, experiments as expected showed that the safe math in assembly{ ... } blocks was not applied. There was no other suspects in the list of changes in 0.8.0 that could have caused such behavior. Closer look at the execution trace showed, that revert happened due to (a == (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF & a)) check. This is basically a check that there are no garbage bits of data in padded calldata word of the a argument. Thus the 0.8.0 compiler now enforces previously ignored ABI specification requirement for pad_right function.

There is a GitHub repository with tests that you can run yourself to see how this check affects the code.


Contact us

Anton Permenev

Anton Permenev

Smart Contract Assurance, PwC Switzerland

Tel: +41 58 792 44 50