4

I am learning Yul for my Bachelor's thesis and I am currently stuck on understanding a small code segment with require and revert functions.

A simple require function in Solidity ...

require(_amountToRaise > 0, "Amount to raise smaller than 0");

... translates to Yul/Inline assembly with ...

if iszero(gt(_amountToRaise, 0)) {
    mstore(0x80, shl(229, 4594637))   // <--- what is this?
    mstore(0x84, 32)                  // <--- and this?
    mstore(0x104, 30)
    mstore(0x124, "Amount to raise smaller than 0")
    revert(0x80, 0x64)
}

EDIT: BE AWARE, as mentioned by the answers, that I have mixed up 0x... offsets. Correction in the two answers below.

I understand that the revert message, and strings as well, need to be encoded by specifying the byte-length (30) in the first 32 byte slot and the characters in the next 32 byte slot. But what about the 4 byte (at 0x80) and 32 byte slot (at 0x84) after? Is it some kind of hashed function signature I am not understanding?

The two lines are always the same with every require/revert function.

Thanks in advance for any help!

2 Answers 2

6

This kind of code:

require(_amountToRaise > 0, "Amount to raise smaller than 0");

Produces revert with bytes equivalent to calldata of the following method:

function Error(string memory reason) external;

ABI-encoded calldata structure is the following:

4 bytes selector
32 bytes offset (value 0x20)
32 bytes string length
N bytes string, optionally padded to 32 bytes

So correct assembly generating this calldata (reason up to 32 bytes):

let ptr := mload(0x40) // Get free memory pointer
mstore(ptr, 0x08c379a000000000000000000000000000000000000000000000000000000000000000) // Selector for method Error(string)
mstore(add(ptr, 0x04), 0x20) // String offset
mstore(add(ptr, 0x24), 30) // Revert reason length
mstore(add(ptr, 0x44), "Amount to raise smaller than 0")
revert(ptr, 0x64) // Revert data length is 4 bytes for selector and 3 slots of 0x20 bytes

For the case of custom error types:

contract Foo {
    error MyError();
    error MyErrorWithValue(uint256);
    error MyErrorWithTwoValues(uint256, uint256);

    function f() external {
        revert MyError();
    }

    function g() external {
        revert MyErrorWithValue(555);
    }

    function h() external {
        revert MyErrorWithTwoValues(555, 666);
    }
}

Assembly equivalent:

function f() external {
    // revert MyError();
    bytes4 errorSelector = this.MyError.selector;
    assembly {
        mstore(0, errorSelector)
        revert(0x1c, 0x04)
    }
}

function f() external {
    // revert MyErrorWithValue(555);
    bytes4 errorSelector = this.MyErrorWithValue.selector;
    assembly {
        mstore(0, errorSelector)
        mstore(4, 555)
        revert(0x1c, 0x24)
    }
}

function h() external {
    // revert MyErrorWithValues(555, 666);
    bytes4 errorSelector = this.MyErrorWithTwoValues.selector;
    assembly {
        let ptr := mload(0x40)
        mstore(ptr, errorSelector)
        mstore(add(ptr, 0x04), 555)
        mstore(add(ptr, 0x24), 666)
        revert(add(ptr, 0x1c), 0x44)
    }
}

In the first two methods revert data fits in scratch space (first 0x40 bytes), but third method doesn't fit and should use free memory pointer (stored in memory at 0x40). As some may think due to revert operation they could use beginning of the memory for storing more than 64 bytes - Solidity documentation states that this can't be considered as memory-safe assembly if it's important in your case.

Using assembly with custom errors could produce issues with compiler generated ABI if this happens in libraries. Compiler 0.8.17 could not include custom error into ABI due non-detection of actual usage this error type. You can track this issue here: https://github.com/ethereum/solidity/issues/13149

2
  • Thank you so much for the clear and thorough answer, further examples and information at the end. It was a godsent! I think I was on the right path, as I thought it looked similar to the decoding of calldata when calling a function (with the 4 byte function signature and so on) but couldn't figure out that it was the decode Error(string). Thanks again! Commented Jan 14, 2023 at 20:19
  • 1
    @EyeEmmaJedi yes, encoding of revert is exactly the same as encoding function call.
    – k06a
    Commented Jan 15, 2023 at 22:44
4

First I think you mixed some hexadecimal notation with some decimal ones, the correct decomposition should be :

if iszero(gt(_amountToRaise, 0)) {
    mstore(0x80, shl(229, 4594637)) 
    mstore(0x84, 32) 
    mstore(0xA4, 30)
    mstore(0xC4, "Amount to raise smaller than 0")
    revert(0x80, 0x64)
}

At a higher level this code is equivalent to :

require(_amountToRaise > 0, "Amount to raise smaller than 0");

Which is actually translated to :

if (amount <= 0) {
   revert("Amount to raise smaller than 0");
}

Where the revert returns an abi encoded custom error "call" for Error(string).

The encoding can be decomposed that way :

 mstore(0x80, shl(229, 4594637))

Writes the function selection for Error(string) at [0x80, 0x84].

mstore(0x84, 32)

Writes the offset of the abi.encoded `string' at [0x84, 0xA4]with respect to the beginning of the encoded parameters (i.e., excluding the function selector), this is a normal behavior for abi.encoded dynamic parameters as you can see here.

mstore(0xA4, 30)
mstore(0xC4, "Amount to raise smaller than 0")

Respectively, write the length of the string and it's content.

1
  • Thank you for your thorough answer as well! I would have loved to mark both answers as a solution, but have gone ahead and marked the other one, as it contained some valuable background information. But I love your answer as well, as it is a quick fix and explanation :) Commented Jan 14, 2023 at 20:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.