Introduction


Hello there!

This is a book about Yul, a low-level, intermediate language written alongside Solidity that can be compiled to bytecode for different backends [1].

Over the course of this book, we are going to go through what Yul is, why it is important, its advantage and disadvantage over Solidity and its implementations in a smart contract.

Also, we will see how storage and memory are laid out in Solidity and how we would harness the power of Yul to infiltrate and modify these to our taste.

Enjoy!

What Is Yul?


Yul is an intermediate language that compiles to Ethereum Virtual Machine (EVM) bytecode. It is designed to be a low-level language that gives developers a high degree of control over the execution of their smart contracts. It is similar in many ways to Assembly but with higher-level features that make it easier to work with [2].

Yul was previously called Julia or Iulia.

Within the context of smart contract development, Yul is usually referred to as Inline Assembly, as it is very similar to Assembly and is written within functions in smart contract code.

Why Is Yul Important?


During the smart contract development process, there are actions, or manipulations which are not feasible for the programmer from the high-level Solidity code. Using Yul, the programmer is given much more fine-grained control over the storage, memory and in some cases calldata layout of the EVM. This control also allows for much more gas efficient code to be written.

The flexibility and gas optimization of Yul are what makes it important.

Yul's Advantage


As stated in the previous chapter, the ability to manipulate the storage, memory or calldata layout to the programmer's taste with an extra benefit of much more gas optimized code is Yul's major advantage over high-level Solidity.

Yul's Disadvantage


As with everything without control limits, the wrong modification, setting or manipulation of the storage or memory layout has the potential of breaching the security of the smart contract, rendering it useless till eternity, or putting users' funds at risk.

For this reason, it is advised that before writing Yul, one must understand the risks involved, and also, the workings of the EVM.

Code Layout


Every piece of Yul code written here can be rerun on Remix.

Remix is an online smart contract development suite. It also offers a way of observing the process of EVM layouts and storage. It will be used over the course of this book, and you can replicate whatever we have done here on the suite.

It is also recommended that you keep evm.codes handy over your course of learning and practising Yul, and even after you've understood the language. It is a magnificent resource.

Authors


Solidity's Storage And Memory


Solidity offers us two locations for data storage:

  • Storage
  • Memory

The storage includes data that persist on the smart contract. All your public, internal and private variables are stored in the storage location.

The memory, however is a temporary storage location that is assigned and cleared on every function call. It doesn't persist.

You can understand more about what the storage and memory locations entail in this article here.

Solidity's Storage And Memory Layout


We have understood what the storage and memory are. Persistent and temporary, respectively. Now we are going to take a look at how these two data storage locations are laid out.

Storage

Solidity's storage layout has a finite amount of space, which is broken down into 32-byte groups called slots, and each slot can hold a 256-bit value. These slots start from index 0 and can stretch to an index limit of (2^256) - 1. It is safe to say that the storage can never get full. Cool, isn't it?

Values stored in storage slots are stored as bytes32 values, and sometimes, can be packed, as we will see later on in this book. To retrieve the value of a Solidity storage variable, the 32-byte value stored in the corresponding slot is retrieved. In some cases - when the value in the slot has been packed -, the value retrieved is worked on by methods of shifting or masking to retrieve the desired value.

💡 Remember when we said one should know how the EVM works before moving on with Yul? Yeah, that's one of the reasons why. You cannot retrieve what you do not know how it was stored.

Memory

Solidity's memory layout, unlike the storage layout is quite tricky. While the storage has a defined maximum slot index of (2^256) - 1 that can hold 32-byte values, the memory is a large group of 32-byte slots that their data can not be retrieved by passing a slot index. But instead, data stored in the memory are retrieved by picking a particular location and returning a specific number of bytes from that point in the memory.

"Why?", you may ask, it is because the default number of bytes returned from any point in memory is 32 and in cases where the point started from is the middle of a particular 32-byte slot, it will encroach into the next slot.

You can imagine memory slots as laid out end to end, in a way that data retrieval can be started from any point and stopped at any point. Unlike storage that returns only the 32-bytes stored at an index, nothing more, nothing less.

If you do not understand that, do not sweat it. You will get a better grasp of it when we talk about variable storage in memory section.

These positions in memory start from 0x00 and are in groups of 32-bytes, meaning that the slots are in th is way:

0x00 - 0x1f
0x20 - 0x3f
0x40 - 0x5f
0x60 - 0x7f
...
0xm0 - 0xnf

According to the Solidity Docs, there are some reserved memory slots for some purposes.

0x00 - 0x3f (64 bytes): Scratch space for hashing methods.
0x40 - 0x5f (32 bytes): Currently allocated memory size (aka. free memory pointer).
0x60 - 0x7f (32 bytes): Zero slot.

Scratch space can be used between statements (i.e. within inline assembly). The zero slot is used as initial value for dynamic memory arrays and should never be written to (the free memory pointer points to 0x80 initially) [3].

💡 Position 0x40 always holds the next free memory location.

💡 It is safest to use mload(0x40) to get the next free memory pointer when trying to store data to memory as storing in a memory location with existing data overwrites that location.

💡 The positions and values in memory that we will use over the course of this book to access memory points will be written in hexadecimals (0x**) as they are easier to read, since the EVM already deals in hexadecimals.

Using Yul To Read And Write Directly To Storage And Memory


Remember when we said keep evm.codes handy? You can go to the website and take a look at some Yul commands. Take note of the mload, mstore, mstore8, sload and sstore.

mload is short for Memory Load.
mstore is short for Memory Store.
mstore8 is short for Memory Store 8-Bits or Memory Store 1-Byte.
sload is short for Storage Load.
sstore is short for Storage Store.

Depending on the location you want to store your value to or read your values from, you have to use one of these.

In the next chapters, we will head into reading from and writing data to our Solidity storage and memory locations.

This is where the fun begins. I hope you like risky fun.

Starting Yul In A Solidity Contract


Yul, as you know by now, is written inside Solidity functions. To start off a Yul code in a Solidity function, you simply declare the assembly keyword, followed by curly braces. Your Yul (Inline Assembly) code can then come inside the curly braces.

assembly {
    // You can now write Yul here.
}

In a proper Solidity function, using the above, you'd have something like this.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function yulFunction() public {
        assembly {
            // Yul code here.
        }
    }
}

You can have more than one assembly block in a function.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function yulFunctions() public {
        assembly {
            // Yul code here.
        }

        assembly {
            // Another Yul code here.
        }
    }
}

Variables in Yul are declared with let keyword. A delcared variable that has not been initialized (y below) defaults to 0, 0x0000000000000000000000000000000000000000000000000000000000000000 in bytes32 until it is assigned a value. They are assigned using the := operator. Most importantly, there are no semicolons in Yul code. Whew!

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function yulFunction() public {
        assembly {
            let x := 5
            let y // Defaults to 0.
            y := 10
        }
    }
}

🚨 Yul does not recognize Solidity global or state variables, it only recognized local variables within functions, function parameters and named return variables.

Variable Storage In Storage


Solidity stores variables declared globally, otherwise known as state variables in storage. The storage is made up of slots, as we have discussed earlier. In this section we will look at how different data types are stored in Solidity's storage. Some are packed and some are greedy enough to take up a full slot without sharing.

You can head into the start of the section at uint8, uint128, uint256.

uint8, uint128, uint256


Unsigned integers are stored in memory based on the size of bytes they have. The size of uint[n] bytes can be realized by dividing [n] by 8, meaning that, while small uint[n] values like uint8 have 1 byte, uint256 has 32 bytes. Solidity's storage is designed in such a way that it can contain up to 32 bytes of value in one slot. In a situation where a variable doesn't contain up to 32 bytes, the value is stored and the next variable will be packed into the same slot, on the condition that when the bytes are added to the slot, it doesn't exceed 32 bytes.

uint256 => 32 bytes => Slot 0.

This is the maximum, hence it occupies an entire slot of its own. Whatever bytes we are going to add will exceed 32 and hence, will pick up the next slot, slot 1.

uint128 => 16 bytes => Slot 1. We still have 16 bytes left to be filled.
uint128 => 16 bytes => Slot 1. Slot 1 is full.

uint8 => 1 byte => Slot 2. There is still 31 bytes left. And as long as the subsequent variable bytes when added to the existing bytes in slot 2 is less than 32, they will all be packed in Slot 2.

Single uint8 Value

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Slot 0.
    uint8 internal smallUint;
    
    function storeUint8() public {
        assembly { 
            // Store the number 16 in slot 0.
            sstore(0x00, 0x10)
        }
    }
    
    function getUint8() public view returns (bytes32) {
        assembly {
            let val := sload(0x00)
            mstore(0x80, val)
            return(0x80, 0x20)
        }
    }
}

Here, we called the sstore command, which takes in two arguments, the slot and the value to store in the slot.

sstore(slot, value)

Slots start from index 0 and increment with every variable added, unless those variables are packed. To know which slot what data is written to, you have to sum up the bytes of all variables, divide by 32 and take the quotient.

8/32 = 0 r 8, hence, slot 0.

To retrieve a value stored at a storage location, the sload keyword is used, and it takes in an argument, the slot to load the data from.

sload(slot)

In a function, to return a value loaded from the storage, without declaring a return variable name, we have to move the value to be returned from storage to memory using mstore which like sstore takes two arguments, what position in memory to store the data at and what data to store. Recall we stated that 0x80 is the start of the free memory that we can play around with. So, our command to the EVM is, store the value loaded val at memory location 0x80. Then, return, 32 bytes of data starting from memory location 0x80.

return is a Yul keyword that takes in two arguments like the others, the position in memory to return from and the size of bytes to return.

return(position, size)

If we called the getUint8 function to return a uint8, Solidity will handle the conversion for us and we will see 16 returned. However, we will be returning in bytes32 over the course of this book to understand the actual storage and memory layouts of the EVM.

Calling the getUint8 function will return 0x0000000000000000000000000000000000000000000000000000000000000010, which when converted to decimal, equals to 16, the value we stored.

To get a glimpse of Solidity doing the automatic conversion of bytes32 to uint8, we can rewrite the code to this:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Slot 0.
    uint8 internal smallUint;
    
    function storeUint8() public {
        assembly { 
            // Store the number 16 in slot 0.
            sstore(0x00, 0x10)
        }
    }
    
    function getUint8() public view returns (uint8 _val) {
        assembly {
            _val := sload(0x00)
        }
    }
}

Variables declared within a function are in the scope of Yul, also, named return variables are within the scope of Yul.

🚨 Only use this method if you are sure that the data returned occupies the entire 32 byte slot. Returning a packed slot would result in wrong returns.

Packed uint8 Value.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    uint8 internal uint1 = 1; // Slot 0
    uint8 internal uint2 = 2; // Slot 0
    uint8 internal uint3 = 3; // Slot 0
    uint8 internal uint4 = 4; // Slot 0
    uint8 internal uint5 = 5; // Slot 0
    
    function getUint8() public view returns (bytes32) {
        assembly {
            let val := sload(0x00)
            mstore(0x80, val)
            return(0x80, 0x20)
        }
    }
}

Calling the getUint8 function returns 0x0000000000000000000000000000000000000000000000000000000504030201, which when observed closely, is a pack of the 5 uint8 variables we have declared in hex format, 01, 02, 03, 04 and 05. And it is nice to observe that they were packed in order of first to last, from right to left.

Retrieving values from this one is not as straight forward as it was, this is where we consider offsets, shifts and masks in Yul.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    uint8 internal uint1 = 1; // Slot 0, offset 0.
    uint8 internal uint2 = 2; // Slot 0, offset 1.
    uint8 internal uint3 = 3; // Slot 0, offset 2.
    uint8 internal uint4 = 4; // Slot 0, offset 3.
    uint8 internal uint5 = 5; // Slot 0, offset 4.
    
    // Return the value of the 5th uint8 variable.
    function getUint8() public view returns (bytes32) {
        assembly {
            let val := sload(0x00)
            let bytesToShiftLeft := sub(0x20, add(uint5.offset, 0x01))
            let leftShift := shl(mul(bytesToShiftLeft, 0x08), val)
            let rightShift := shr(mul(0x1f, 0x08), leftShift)
            mstore(0x80, rightShift)
            return(0x80, 0x20)
        }
    }
}

You can get the offset of a variable in storage by passing <variableName>.offset. And likewise, you can get the slot of any variable in storage by passing <variableName>.slot. Easier than calculating, isn't it?

If you can understand the layout of the data you're returning, you can then manipulate it to return what you desire. We will see how this works when we take a look at arrays. If you can't understand it at the moment, do not fret. Practise, play around with offset and shift.

🚨 Understand the layout of your values before using Yul to manipulate them.

Single uint128 Value

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Slot 0.
    uint128 internal mediumUint;
    
    function storeUint128() public {
        assembly { 
            sstore(0x00, 0x1234567890)
        }
    }
    
    function getUint128() public view returns (bytes32) {
        assembly {
            let val := sload(0x00)
            mstore(0x80, val)
            return(0x80, 0x20)
        }
    }
}

Returns 0x0000000000000000000000000000000000000000000000000000001234567890.

Packed uint128 Value

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Slot 0.
    uint128 internal mediumUint = 9840913809138;
    uint128 internal mediumUint2 = 9304137410311;
    
    function getUint128() public view returns (bytes32) {
        assembly {
            let val := sload(0x00)
            mstore(0x80, val)
            return(0x80, 0x20)
        }
    }
}

Returns 0x00000000000000000000087649ce27070000000000000000000008f3442bfef2.

Single uint256 Value

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Slot 0.
    uint256 internal bigUint;
    
    function storeUint256() public {
        assembly { 
            sstore(0x00, 0x1234567890abcdef)
        }
    }
    
    function getUint256() public view returns (bytes32) {
        assembly {
            let val := sload(0x00)
            mstore(0x80, val)
            return(0x80, 0x20)
        }
    }
}

Returns 0x0000000000000000000000000000000000000000000000001234567890abcdef.

int8, int128, int256


While uint[n] types can only contain positive integers, the int[n] types can contain both positive and negative numbers. As you know, int[n] variables store numbers ranging from -1 -> -(2 ^ (n-1)) on the negative side, and 0 -> ((2^n) - 1) on the positive side. This means that for the three int[n] types specified above, this would be their minimum and maximum values.

int[n] typeMinimum ValueMaximum Value
int8-128127
int128-2^127(2^127) - 1
int256-2^255(2^255) - 1

When it comes to storage of int[n] types, the positive values range from 0x00 in hex, (with the number of bytes after 0x corresponding to the number of bytes of the int[n] type) up till 0x7f. While the negative numbers range from 0xff to 0x80 backwards. It is reasonable as 0xff is greater than 0x80 on hexadecimal face value, so is -1 greater than -128 on the decimal scale.

ValueValueValueValueValueValue
012...125126127
0x000x010x02...0x7d0x7e0x7f
-1-2-3...126127-128
0xff0xfe0xfd...0x820x810x80

int[n] types are packed in a similar way, just like their uint[n] counterparts.

You can try to store different int[n] types on Remix to see their storage layouts.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // int<n> public variableName = value;
    int8 public int8Value = -1; // Slot 0.

    function getInt8() public view returns (bytes32) {
        assembly {
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }
}

🚨 Due to the tricky nature of the storage of int[n] types, apply more care when storing and manipulating values from storage.

🚨 Do not store negative int[n] values directly from your Yul block of code, Yul treats it as a uint[n] type overflow, meaning that -1 will be converted to (2^256) - 1. This can lead to security breaches.

bytes1, bytes16, bytes32


bytes[n] are fixed length byte arrays, as opposed to bytes that are variable length byte arrays. The storage of bytes[n] in storage is such that it takes up a slot when it is of bytes32 and is packed in cases of bytes below 32. It is very similar to the uint type.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    bytes1 public a = 0xaa;
    bytes1 public b = 0xbb;
    bytes1 public c = 0xcc;
    
    function getBytes1() public view returns (bytes32) {
        assembly {
            let val := sload(0x00)
            mstore(0x80, val)
            return(0x80, 0x20)
        }
    }
}

Returns 0x0000000000000000000000000000000000000000000000000000000000ccbbaa, just like in uint's case.

However, there is some bit of caution to be proceeded with when dealing with bytes[n]. Take a look at these three pieces of code written using bytes4.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    bytes4 public a = 0xaabbccdd;
    
    function getBytes4() public view returns (bytes32) {
        assembly {
            let val := sload(0x00)
            mstore(0x80, val)
            return(0x80, 0x20)
        }
    }
}

A call to the getBytes4 function would return 0x00000000000000000000000000000000000000000000000000000000aabbccdd, which is what we expect, according to what we have learnt.

Take a look at the second one.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    bytes4 public a;
        
    function setAndReturnBytes4() public returns (bytes32) {
        bytes4 f = 0xaabbccdd;

        assembly {
            sstore(0x00, f)
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }
}

This will return 0xaabbccdd00000000000000000000000000000000000000000000000000000000. You can find this by checking the Remix Console, expanding the transaction and checking the decoded output object. This is wrong, and will set the value of a to 0x00000000.

And a look at the final one.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    bytes4 public a;
        
    function setAndReturnBytes4() public returns (bytes32) {
        assembly {
            sstore(0x00, 0xaabbccdd)
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }
}

This will return 0x00000000000000000000000000000000000000000000000000000000aabbccdd.

Setting a bytes[n] variable in a function will right-pad it to 32 bytes. Whereas, directly assigning the variable in a Yul block or in storage will handle it normally by left padding it.

To get a knowledge of which type of bytes[n] does what, refer to this part of the book.

bytes16

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    bytes16 public a;
        
    function setAndReturnBytes16() public returns (bytes32) {
        assembly {
            sstore(0x00, 0x0011223344556677889900aabbccddeeff)
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }
}

Returns 0x0000000000000000000000000000000011223344556677889900aabbccddeeff.

bytes32

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    bytes32 public a;
        
    function setAndReturnBytes32() public returns (bytes32) {
        assembly {
            sstore(0x00, 0x003344556677889900aabbccddeeff0011223344556677889900aabbccddeeff)
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }
}

Returns 0x003344556677889900aabbccddeeff0011223344556677889900aabbccddeeff.

bytes


bytes are dynamic byte arrays. While the length of a bytes[n] is known as n, bytes do not have a specific length and can stretch up to 128 or even more.

For example, try running the getByteCode() in the ByteCodeGenerator contract below on Remix.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract MyContract {
    uint16 internal a;
    uint16 internal b;
    string internal c;
    uint256 internal d;

    struct E {
        bytes32 f;
    }

    E internal e;

    constructor(uint256 _d) {
        d = _d;
    }
}

contract ByteCodeGenerator {
    function getByteCode() public pure returns (bytes memory) {
        bytes memory bytecode = type(MyContract).creationCode;
        return abi.encodePacked(bytecode, abi.encode(123));
    }
}

As observed, it will return a chunk of bytes in this format that has a length of 249.

0x6080604052348015600e575f80fd5b5060405160d938038060d98339818101604052810190602c9190606a565b80600381905550506090565b5f80fd5b5f819050919050565b604c81603c565b81146055575f80fd5b50565b5f815190506064816045565b92915050565b5f60208284031215607c57607b6038565b5b5f6087848285016058565b91505092915050565b603e80609b5f395ff3fe60806040525f80fdfea2646970667358221220e7af087555eba8f8a284453d72cfff0475dbf8f637b5a2d261a027c32bdfa10764736f6c63430008190033000000000000000000000000000000000000000000000000000000000000007b

Once again, bytes can have an arbitrary length.

Storage

The storage for bytes goes two ways.

  • Storage for bytes with length from 31 and below.
  • Storage for bytes with length from 32 and above.

Storage For bytes With Length From 31 And Below.

This is very simple. Two factors are taken into consideration, the length of the bytes and the corresponding slot.

💡 bytes are counted in twos. This means that 0xab is a bytes with a length of 1, and 0xabcd is a bytes with a length of 2.

To store this type of bytes, the 32-byte storage slot is broken up into two. One part to hold the 31-length bytes and the other part to hold the length of the bytes, calculated as length * 2 [4].

In other words, this,
0x0000000000000000000000000000000000000000000000000000000000000000
(32 bytes)
is broken into,
0x00000000000000000000000000000000000000000000000000000000000000 - 00
(31 bytes - 1 byte).

The 31 bytes section would hold the actual bytes value passed, from left to right, while the 1 byte would hold the length as calculated above, length * 2.

💡 Storage slot bytes go from the highest order to the lowest order. Meaning that for a bytes 32 storage slot, 0x0000000000000000000000000000000000000000000000000000000000000000, 0x00 Is the highest order byte, while the last 00 is the lowest order byte.

To store a simple bytes value, let's say 0xabcdef in storage, it would be stored in this way, 0xabcdef0000000000000000000000000000000000000000000000000000000006. Recall what we have said and let's apply accordingly. The length of the bytes is 3 (ab, cd, ef), and 3 * 2 = 6. 6 when converted to hexadecimals is 0x06, and we can see that in the last byte of the storage slot's value. And the actual bytes is then stored in the remaining 31 bytes, from the highest order to the lowest order.

bytes will always occupy one storage slot, and they cannot be packed with any other type of data type.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract MyContract {
    // Slot 0.
    bytes public myBytes = hex'01_23_45_67_89_ab_cd_ef';
    
    function getBytes() public view returns (bytes32) {
        assembly {
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }
}

Returns 0x0123456789abcdef000000000000000000000000000000000000000000000010.

💡 You can separate 0x0123456789abcdef000000000000000000000000000000000000000000000010 into 0x0123456789abcdef0000000000000000000000000000000000000000000000 (31 bytes) and 10 (1 byte) and study how they were gotten. It's the same thing as the one we did above.

Storage For bytes With Length From 32 And Above.

Due to the fact that the length of the bytes is greater than 31, there would not be enough room in a single storage slot to store the length data and the actual bytes value. Therefore, a different approach is used.

The length data is stored at the slot, and, unlike bytes with lengths of 31 and below, the length data for bytes with lengths of 32 and above is calculated as (length * 2) + 1. This value is stored at the corresponding slot for the bytes variable. And the value for the bytes is then stored at keccak256(slot). If we are to store a bytes variable with length 32 and a value at slot 0, slot 0 would hold the value of 65 ((32 * 2) + 1) while the actual bytes value will be stored at keccak256(0). The keccak256 has is calculated using Yul, and not Solidity [4].

If the length of the bytes value exceed 32, they overflow in to the next storage slots. Meaning that, if we have a bytes with a length of 40, corresponding to slot 0, the value would be found at keccak256(0), but, we would only see 32 bytes of the entire thing, while the remaining 8 bytes would be in the next slot, keccak256(0) + 1

We're going to store a pretty long bytes value now.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract MyContract {
    // Slot 0.
    bytes public myBytes = hex'01_23_45_67_89_ab_cd_ef_01_23_45_67_89_ab_cd_ef_01_23_45_67_89_ab_cd_ef_01_23_45_67_89_ab_cd_ef_01_23_45_67_89_ab_cd_ef';

    function getBytes() public view returns (bytes32) {
        assembly {
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }
}

getBytes returns 0x0000000000000000000000000000000000000000000000000000000000000051. Converting this to decimal would be 81. Subtracting 1 and dividing by 2 gives us 40, which is the actual length of the string. As explained earlier, this would not fit in one storage slot and would be stored in two slots, which would be found at keccak256(0) and keccak256(0) + 1 respectively.

We'd write the Yul code to return them separately.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract MyContract {
    // Slot 0.
    bytes public myBytes = hex'01_23_45_67_89_ab_cd_ef_01_23_45_67_89_ab_cd_ef_01_23_45_67_89_ab_cd_ef_01_23_45_67_89_ab_cd_ef_01_23_45_67_89_ab_cd_ef';
    
    function getBytes() public view returns (bytes32) {
        assembly {
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }

    function getFirstBytesSection() public view returns (bytes32) {
        assembly {
            mstore(0x80, 0x00)
            let location := keccak256(0x80, 0x20)
            mstore(0x80, sload(location))
            return(0x80, 0x20)
        }
    }

    function getSecondBytesSection() public view returns (bytes32) {
        assembly {
            mstore(0x80, 0x00)
            let location := keccak256(0x80, 0x20)
            let nextLocation := add(location, 1)
            mstore(0x80, sload(nextLocation))
            return(0x80, 0x20)
        }
    }
}

getFirstBytesSection would return 0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef, which is the first 32 bytes of the value we stored, while, getSecondBytesSection would return 0x0123456789abcdef000000000000000000000000000000000000000000000000, which is the next 32 bytes, but as observed, it only contains values for the first 8 bytes. Using both data, we can see that our bytes value was spread out across two storage slots. We can try to concatenate them and return all the bytes, but that would involve moving them into memory, and then returning the proper ABI encoded memory data for the bytes. We would look at that when we get to the Variable Storage In Memory section of this book.

💡 bytes and string share the same characteristics in storage and memory. The same way bytes are stored in storage and memory, that is the same way string are stored. The only differences are that each string character is converted into their hexadecimal components before storage. And that while bytes characters are counted in twos, string characters are counted singly, like you do with your everyday words.

string


string is first converted into its bytes type by converting each individual ASCII character to its hexadecimal value. It is then stored in the exact way bytes are stored.

Example, a string type, "hello" would be first converted into 0x68656c6c6f, a concatenation of each hexadecimal value of each character, and then stored just the way bytes are stored.

To get a knowledge of what the hexadecimal values of ASCII characters are, you can look up this table at RapidTables.com.

Refer to bytes.

address


address is a bytes20 or uint160 value, it holds 20 bytes of data. Since address take up 20 bytes of a storage slot, they can be packed with any number of types that can sum up to 12 bytes.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    address public a;
        
    function setAndReturnAddress() public returns (bytes32) {
        assembly {
            // My Remix address is `0x5B38Da6a701c568545dCfcB03FcB875f56beddC4`.
            sstore(0x00, caller())
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }
}

Returns 0x0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4. As seen, the last 20 bytes of the bytes32 corresponds to the address I made the contract call with. caller() in Yul equates to msg.sender in Solidity.

To understand how address, bytes20 and uint160 types co-relate, read up this section from the Solidity documentation.

struct


A struct is a group of data. The layout of a struct is identical to the layout of the data within a struct. The slots a struct would occupy is dependent on the types of variables within the struct. A struct with two uint256 types would occupy two slots.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract SimpleStruct {
    struct S {
        uint256 a;          // Slot 0.
        uint256 b;          // Slot 1.
        uint256 c;          // Slot 2.
        address owner;      // Slot 3.
        bytes12 structHash; // Slot 3.
    }

    S public s;
}

The storage and retrieval of data from a struct aligns with the storage and retrievals of what we have discussed so far.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    struct S {
        uint256 a;          // Slot 0.
        uint256 b;          // Slot 1.
        uint256 c;          // Slot 2.
        address owner;      // Slot 3.
        bytes12 structHash; // Slot 3.
    }

    S public s;
    
    constructor() {
        s = S(
            10, 15, 20,
            msg.sender,
            bytes12(keccak256(abi.encode("Hello World!")))
        );
    }
    
    function getStructSlotValue(uint8 slot) public view returns (bytes32) {
        assembly {
            mstore(0x80, sload(slot))
            return(0x80, 0x20)
        }
    }
}

We are trying to retrieve the data in the struct, and since the struct occupies 4 slots, we want to make the data retrieval flexible, allowing the user to pass which slot they want to retrieve its value, using the slot parameter.

😉 Try to retrieve slot number 3 and figure out how it was packed. Then, try to study slots 0, 1 and 2.

If a struct contains a bytes or a string type, it is stored the same way it is supposed to be.

mapping


A typical mapping in Solidity is accessed using the key. There are two types of mapping as you know, single mapping and nested mapping. We will look at how to retrieve data from a single and a nested mapping.

Single mapping

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    mapping(address => uint256) public myMap;
    
    function getMap() public view returns (uint256) {
        return myMap[msg.sender];
    }
}

In the getMap function, the msg.sender is the key.

In Yul, mappings take up a full slot. They cannot be packed with any other variable. And to access a mapping key value, the value is stored at a location calculated as the keccak256(key, slot). In other words, to access the value of a key in a mapping, we must know the corresponding slot of the mapping, and the key we want to look for. Then, we store the key first in the first 32 bytes of free memory, and store the slot in the next 32 bytes of free memory, and then hash the entire thing from the start of the first memory containing the key to the end of the second memory containing the slot. This will always be a constant size of 64 bytes, 0x40 in hexadecimals.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    uint64 public TIME = uint64(block.timestamp);   // Slot 0.
    mapping(address => uint256) public myMap;       // Slot 1.

    constructor() {
        myMap[msg.sender] = TIME;
    }

    function getMap() public view returns (bytes32) {
        assembly {
            // Get free memory location.
            let freeMemLoc := mload(0x40)
            // Store our key, `msg.sender` at the free memory.
            mstore(freeMemLoc, caller())
            // Set the next free memory to be the start of the next 32-byte slot location,
            // as we've stored the `msg.sender` in the current freeMemLoc location.
            let nextFreeMemLoc := add(freeMemLoc, 0x20)
            // Store the slot of the mapping (1) in the next free memory location.
            mstore(nextFreeMemLoc, 0x01)
            // Hash from start to end.
            let keyLocation := keccak256(freeMemLoc, 0x40)
            // Get the value of the key in the mapping.
            let value := sload(keyLocation)

            // Store value in memory and return.
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }
}

Nested mapping

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    mapping(address => mapping(uint256 => uint256)) public myNestedMap;
    
    function getMap(uint256 index) public view returns (uint256) {
        return myNestedMap[msg.sender][index];
    }
}

A nested mapping can have two or more keys.

To load data from a nested mapping, the number of hashes to be done must be equal to the number of keys in the map. As we saw earlier, our previous single mapping had one key, and we did only one hash. A mapping with two keys will require two hashes to get to the part of storage holding its value. The first hash, would be exactly as the one above where we hash a memory concatenation of the first key and the slot. The corresponding hashes would be a concatenation of the next key and the hash value of the previous hash.

Let us see what we mean. We will try to store a number on index 5 of the msg.sender and we will retrieve it using Yul.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    uint8 public INDEX = 5;                                             // Slot 0.
    uint64 public TIME = uint64(block.timestamp);                       // Slot 0.
    mapping(address => mapping(uint256 => uint256)) public myNestedMap; // Slot 1.

    constructor() {
        myNestedMap[msg.sender][INDEX] = TIME;
    }
    
    function getNestedMap() public view returns (bytes32) {
        assembly {
            // Get free memory location.
            let freeMemLoc := mload(0x40)
            // Store our first key, `msg.sender` at the free memory.
            mstore(freeMemLoc, caller())
            // Set the next free memory to be the start of the next 32-byte slot location,
            // as we've stored the `msg.sender` in the current freeMemLoc location.
            let nextFreeMemLoc := add(freeMemLoc, 0x20)
            // Store the slot of the mapping (1) in the next free memory location.
            mstore(nextFreeMemLoc, 0x01)
            // Hash from start to end.
            let innerHash := keccak256(freeMemLoc, 0x40)
            // This is the first hash retrieved. To get the actual location, there
            // will be a second hash.
            // We simply repeat the process as above, only concatenating the next
            // key and the previous hash value.
            // INDEX == 5, `0x05` in hexadecimal.
            mstore(freeMemLoc, 0x05)
            mstore(nextFreeMemLoc, innerHash)
            let location := keccak256(freeMemLoc, 0x40)
            // Get the value of the key in the mapping.
            let value := sload(location)

            // Store value in memory and return.
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }
}

Depending on how many keys are in your nested mapping, you simply have to repeat the entire process.

enum


enum is equivalent to uint8, the value of an enum variable cannot exceed 256. enum shares the same characteristic as the uint8 type. They can be packed in storage.

Refer to uint8, uint128, uint256.

Custom Types


A custom type or a user-defined value type allows you to create an alias of a native type in Solidity. This alias, will inherit and act as if it is the original type. It is defined by using the type C is V syntax, where C is the custom type, and V is the native type. To convert from the underlying type to the custom type, C.wrap(<value>) is used, and to convert from the custom type to the native underlying type, C.unwrap(<value>) is used [5].

Custom types behave like their underlying types.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    type CustomTypeUint8 is uint8;      // Will behave like uint8.
    type CustomTypeUint256 is uint256;  // Will behave like uint256.
    type CustomTypeInt128 is int128;    // Will behave like int128.
    type CustomTypeAddress is address;  // Will behave like address.
    type CustomTypeBytes4 is bytes4;    // Will behave like bytes4.
    type CustomTypeBytes32 is bytes32;  // Will behave like bytes32.
    
    // Slot 0.
    CustomTypeUint256 public customType = CustomTypeUint256.wrap(12000);
    // Slot 1.
    uint256 public underlyingType = CustomTypeUint256.unwrap(customType);
    
    function getCustomType() public view returns (bytes32) {
        assembly {
            mstore(0x80, sload(0x00))
            return(0x80, 0x20)
        }
    }

    function getUnderlyingType() public view returns (bytes32) {
        assembly {
            mstore(0x80, sload(0x01))
            return(0x80, 0x20)
        }
    }
}

🚨 You cannot define custom types for bytes and string.

Arrays


General Array Storage

Up till now, we've learnt about individual data types in Solidity and how they are stored in storage. Before we proceed to their array counterparts, we would want to go over how arrays are viewed in Solidity storage generally. This view is applied to all other types.

Solidity recognizes two types of arrays, the fixed length array and the dynamic array. These two array types are treated differently by Solidity.

Fixed Arrays, <type>[n]

Solidity views <type>[n] array elements as individual values. Which means that, these values are treated as if they were not in an array. If a uint256[5] array has 5 elements, they will occupy 5 slots, in their correct places.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Slots 0 - 4.
    uint256[5] public fixedArray;
    // Slot 5.
    uint8[6] public fixedSmallArray;
}

In the code above, the fixedArray variable occupies 5 slots. This is because, it contains 5 uint256 values. In Solidity, because the length of the array is fixed (in this case, 5), Solidity knows how much in storage to allocate for the storage of each individual value. It is seen as if they're 5 uint256 values kept side by side.

The fixedSmallArray occupies one slot, because, as explained, it will be seen as 6 uint8 values kept side by side and they, like we have already discussed, will be packed into one slot.

Dynamic Arrays, <type>[]

Dynamic arrays are stored just like fixed arrays, when it comes to packing, but in terms of knowing where in storage to store the array, a little bit of calculation is done. Because the length is dynamic, Solidity does not know how much space to allocate for the storage, therefore, the storage of a dynamic array starts at keccak256(slot) . Meaning that, if a dynamic array is declared at slot 0, the first element will be found keccak256(0).

To read the value of the other array elements from storage, they will be obtained by loading the storage at keccak256(slot) + elementIndex. Meaning that, if we had the above dynamic array that grew to 10 elements, and we would like to retrieve the value of the 9th element, it would be found at keccak256(0) + 9.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Slot 0.
    uint256[] public dynamicArray;
    // Slot 1.
    uint8[] public dynamicSmallArray;
}

The values of the elements in the dynamicArray variable can be found at keccak256(0) + elementIndex. While the values for the dynamicSmallArray will be found at keccak256(1), we didn't add any elementIndex here because the elements of dynamicSmallArray will be packed in one slot because they're uint8 and will be packed. If they are more than enough to fit into the next slot, then, we can load the next storage location.

General <type>[] Deduction.

Once the concept of type storages is understood, you can use that to figure out how the array versions of that type will be stored.

To retrieve an element from a packed array is quite tricky and is not readily advised.

🚨 The use of Yul to read and write arrays is not advised. It is a very tricky business, considering the fact that small types are packed and large types occupy one slot, it is a whole new level of stress to take in packing, and other considerations while storing values into an array from Yul. Allow Solidity to handle the intricacies for you.

Variable Storage In Memory


Variables declared inside a function's body, or returned by a function are stored in memory, except in special cases with libraries, internal and private functions that can return variables stored in storage. The memory, unlike the storage is not divided up into slots, but rather is a vast area of data grouped into 32 bytes. The retrieval of data from memory is even more tricky than that of storage, because, unlike storage that restricts us to read from and write to one slot at a time, we can read from anywhere in memory and write to anywhere in memory arbitrarily. We can start our data reading from location 0x00, or 0x78, or 0x0120, or 0x3454. The memory is very similar to the calldata in terms of data storage. They both have the same layout, the only difference being that the calldata is prepended with 4 bytes of data that we know as the function signature.

When a value is stored in memory, Solidity automatically stores the highest variant of its type in memory. Meaning that storing a data of type uint8 will store a uint256 type of the same number in memory, occupying an entire 32-byte memory space.

Data in the memory and calldata are stored according to the standard ABI specification [3], and they are not packed.

In this section, we will take a look at the different ways data types are stored in memory.

Head into the start of the section at uint8, uint128, uint256.

uint8, uint128, uint256


The storage of uint8, uint128, uint256 in memory are very simple, they are written directly at our chosen memory location and are returned the same.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function uint8InMemory(uint8 value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }

    function uint128InMemory(uint128 value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }

    function uint256InMemory(uint256 value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }
}

int8, int128, int256


The storage of int8, int128, int256 in memory are very simple, they are written directly at our chosen memory location and are returned the same. Yul wraps around negative int[n] values.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function int8InMemory(int8 value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }

    function int128InMemory(int128 value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }

    function int256InMemory(int256 value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }
}

🚨 Due to the tricky nature of the storage of int[n] types, apply more care when storing and manipulating values from storage.

🚨 Do not store negative int[n] values directly from your Yul block of code, Yul treats it as a uint[n] type overflow, meaning that -1 will be converted to (2^256) - 1. This can lead to security breaches. Yul wraps around negative int[n] values.

bytes1, bytes16, bytes32


The storage of bytes1, bytes16, bytes32 in memory are quite different, they are right-padded to 32 bytes if they're not up to 32 bytes, and then, they are written directly at our chosen memory location and are returned with the padding.

Padding?

Padding is the process of adding a number of 0 to a data type's hexadecimal value to make it up to 32 bytes so that it can be written to a location in storage or memory. If the value is up to 32 bytes, it is not padded.

As you must've observed:

  • bytes[n] data written directly to storage from the global variable declaration are left padded, 0x<00..00><value>.
  • bytes[n] data written directly to storage from Yul are left padded, 0x<00..00><value>.
  • bytes[n] data written directly to memory from the global variable declaration are left padded, 0x<00..00><value>.
  • bytes[n] data written directly to memory from Yul are left padded, 0x<00..00><value>.

  • bytes[n] data declared inside a function body and then written to storage are right padded, 0x<value><00..00>.
  • bytes[n] data declared as a function parameter and then written to memory are right padded, 0x<value><00..00>.
  • bytes[n] data declared as a constant and then written to memory are right padded, 0x<value><00..00>.

  • Everything else is left padded.

😉 Keep the above in mind.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function bytes1InMemory(bytes1 value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }

    function bytes16InMemory(bytes16 value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }

    function bytes32InMemory(bytes32 value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }
}

😉 Everything returned here will be right padded, 0x<value><00..00>.

bytes


The storage of bytes in memory is very tricky. Normally, declaring a bytes variable in memory will be followed by the memory keyword as in bytes memory <variableName> this is because, bytes values are encoded in memory. The encoding has three parts.

  1. The pointer.
  2. The length.
  3. The value.

By declaring the bytes memory <variableName>, <variableName> is the pointer. In memory, we can call mload(<variableName>) to load the pointer. The length of the bytes value is stored in the <variableName> pointer. And the value is stored immediately after that. Therefore, reading the next memory chunk after the length chunk is the actual value, meaning that if we read location<variableName> + 32 bytes in memory, we get the actual value (If <variableName> is 0x20 in memory, it holds the length, and 0x40 will hold the value, 0x20 is 32 bytes). Using the length recovered from mload(<variableName>), we can know how many bytes of data we can read from the value location.

To encode bytes in memory (or calldata), we need to know the length of the bytes to return. Then, we pick a part of memory we desire and take note of the location, let's call there 0xa0. At memory location 0xa0, we store 0x20, this is the pointer. At the next 32 bytes, 0xc0, we store the length of the bytes. And at the next 32 bytes, 0xe0, we store the bytes value.

Finally, we return the data starting from 0xa0 where we started and reading 96 bytes. 96 bytes? Yes, 0xa0 to 0xbf is 32 bytes holding our pointer, 0xc0 to 0xdf is another 32 bytes holding our length, and 0xe0 to 0xff another 32 bytes holding the data.

If the bytes value is greater than 32 bytes, we write to more locations after 0xe0 and read the corresponding size in bytes.

Declaring bytes memory <variableName> automatically creates a pointer and length for you depending on the value you pass as the value of the variableName variable. We can simply return whatever it is. The actual bytes value are written the same way in storage, 0x<value><00..00>.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function bytesInMemory(bytes memory value) public pure returns (bytes32) {
        assembly {
            // Value is pointer.
            let valueLoc := add(value, 0x20)
            let bytesValue := mload(valueLoc)
            mstore(0x80, bytesValue)
            return(0x80, 0x20)
        }
    }
}

string


string is first converted into its bytes type by converting each individual ASCII character to its hexadecimal value. It is then stored in the exact way bytes are stored.

Example, a string type, "hello" would be first converted into 0x68656c6c6f, a concatenation of each hexadecimal value of each character, and then stored just the way bytes are stored.

To get a knowledge of what the hexadecimal values of ASCII characters are, you can look up this table at RapidTables.com.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function stringInMemory(string memory value) public pure returns (bytes32) {
        assembly {
            let valueLoc := add(value, 0x20)
            let bytesValue := mload(valueLoc)
            mstore(0x80, bytesValue)
            return(0x80, 0x20)
        }
    }
}

Refer to bytes.

address


The storage of address in memory are very simple, they are written directly at our chosen memory location and are returned the same.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function addressInMemory(address value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }
}

Custom Types


Custom types behave like their underlying types.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    type Address is address;

    function addressInMemory(Address value) public pure returns (bytes32) {
        assembly {
            mstore(0x80, value)
            return(0x80, 0x20)
        }
    }
}

Arrays


Fixed Arrays, <type>[n]

The storage of fixed arrays in memory is simply a concatenation of the storage of its individual elements.

Dynamic Arrays, <type>[]

This follows the three part of memory storage, the

  1. The pointer.
  2. The length.
  3. The value.

By declaring the <type>[] memory <variableName>, <variableName> is the pointer. In memory, we can call mload(<variableName>) to load the pointer. The length of the array is stored in the <variableName> pointer. And we can then go over each 32 byte chunk as much as the length permits to get all values.

Let's take a look.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function addressInMemory(uint8[] memory values) public pure returns (bytes32) {
        assembly {
            mstore(0x80, mload(add(values, 0x20))) // First array value.
            return(0x80, 0x20)
        }
    }

    function addressInMemory2(uint8[] memory values) public pure returns (bytes32) {
        assembly {
            mstore(0x80, mload(add(values, 0x40))) // Second array value.
            return(0x80, 0x20)
        }
    }
}

Order Of Memory And Calldata Storage


In a situation where we have a group of types to be stored in memory or calldata, say a function like this:

function functionName(uint8 value1, string memory stringVal, address[] memory addresses) {}

What's the proper way to encode this in calldata? To find out, the Solidity team have provided a fantastic material in this section of their docs to help with that.

struct


Struct in memory is stored the using the same three-step method.

  1. The pointer.
  2. The length.
  3. The value.

The value is then the proper encoding of its individual types according to the specification stated here by the Solidity team.

Yul Actions


In this section of the book, we will take a look at how to do basic stuff in Yul. From basic things like addition to loops, to hashing, checking of balances and even signature verification.

Feel free to recreate or rerun the codes written here on Remix.

💡 Solidity functions written from here on will return bytes32 to basically show the hexadecimals stored in either storage or memory. You can change the return types and rerun the code on Remix and Solidity will automatically cast it to the return type.

Addition


// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function add(uint256 x, uint256 y) public pure returns (bytes32) {
        assembly {
            mstore(0x80, add(x, y))
            return(0x80, 0x20)
        }
    }
}

The add(a, b) Yul command takes in two numbers as arguments and returns their sum, a + b.

🚨 Additions in Yul are unchecked, meaning that, Yul wraps around numbers if they exceed the highest possible number to be stored in a slot.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function add() public pure returns (bytes32) {
        assembly {
            let large := 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
            mstore(0x80, add(large, 0x03))
            return(0x80, 0x20)
        }
    }
}

The above will return 0x0000000000000000000000000000000000000000000000000000000000000002 showing a wrap around.

To write a Yul addition code to check for overflows and prevent them, this will be the approach to use [6].

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function add(uint256 x, uint256 y) public pure returns (bytes32) {
        assembly {
            if lt(add(x, y), x) {
                revert(0x00, 0x00)
            }

            mstore(0x80, add(x, y))
            return(0x80, 0x20)
        }
    }
}

Subtraction


// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function subtract(uint256 x, uint256 y) public pure returns (bytes32) {
        assembly {
            mstore(0x80, sub(x, y))
            return(0x80, 0x20)
        }
    }
}

The sub(a, b) Yul function takes in two numbers as arguments and returns the difference between the two numbers, a - b.

🚨 Subtractions in Yul are unchecked, meaning that, Yul will also wrap around numbers if the result of the subtraction goes lower than the 0 when subtracting, that is if b > a.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function subtract() public pure returns (bytes32) {
        assembly {
            let small := 0x00
            mstore(0x80, sub(small, 0x02))
            return(0x80, 0x20)
        }
    }
}

The above function will return 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe.

To write a safe Yul subtraction code, this is the best approach [6].

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function subtract(uint256 x, uint256 y) public pure returns (bytes32) {
        assembly {
            if gt(y, x) {
                revert(0x00, 0x00)
            }
            
            mstore(0x80, sub(x, y))
            return(0x80, 0x20)
        }
    }
}

The gt(a, b) Yul function takes in two numbers as arguments and returns 1 meaning true if a > b and 0meaning false if a < b.

Multiplication


// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function multiply(uint256 x, uint256 y) public pure returns (bytes32) {
        assembly {
            mstore(0x80, mul(x, y))
            return(0x80, 0x20)
        }
    }
}

The mul(a, b) Yul command takes in two numbers as arguments and returns their product, a * b.

🚨 Multiplications in Yul are unchecked, meaning that, Yul wraps around numbers if they exceed the highest possible number to be stored in a slot.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function multiply() public pure returns (bytes32) {
        assembly {
            let large := 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
            mstore(0x80, mul(large, 0x12345678))
            return(0x80, 0x20)
        }
    }
}

The above will return 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffedcba988 showing a wrap around.

To write a Yul multiplication code to check for overflows and prevent them, this will be the approach to use [6].

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function multiply(uint256 x, uint256 y) public pure returns (bytes32) {
        assembly {
            if eq(x, 0) {
                return(0x80, 0x20)
            }

            if eq(y, 0) {
                return(0x80, 0x20)
            }

            let z := mul(x, y)
            if iszero(eq(div(z, x), y)) { revert(0x00, 0x00) }

            mstore(0x80, z)
            return(0x80, 0x20)
        }
    }
}

The eq(a, b) Yul function takes in two numbers as arguments and returns 1 meaning true if a == b and 0meaning false if a != b. The iszero(a) function takes in one number as arguments and returns 1 if a == 0 and 0 if a = 0. The eq(a, b) is used to check the truthiness of an expression, while, the iszero(a) is the method used to check the falsiness of an expression. The revert(0x<a>, 0x<b>) reverts the operation with some error message encoded according to the ABI Specification stored starting from positon 0x<a> and spanning 0x<b> bytes.

Division


// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function divide(uint256 x, uint256 y) public pure returns (bytes32) {
        assembly {
            mstore(0x80, div(x, y))
            return(0x80, 0x20)
        }
    }
}

The div(a, b) Yul command takes in two numbers as arguments and returns their quotient, a / b.

To write a Yul divion code to check for zero divisions and prevent them, this will be the approach to use [6].

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function divide(uint256 x, uint256 y) public pure returns (bytes32) {
        assembly {
            if eq(y, 0) {
                revert(0x00, 0x00)
            }
            
            if eq(x, 0) {
                return(0x80, 0x20)
            }

            let z := div(x, y)

            mstore(0x80, z)
            return(0x80, 0x20)
        }
    }
}

Bitwise


Bitwise operations involve left shift, right shift, and, or, xor and not.

Left Shift, shl(a, b)

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function shiftLeft(uint256 num, uint256 bitsToShift) public pure returns (bytes32) {
        assembly {
            mstore(0x80, shl(bitsToShift, num))
            return(0x80, 0x20)
        }
    }
}

shl(a, b) is a Yul function that takes in two arguments, a, the number of bits b would be shifted by, and b, the number to shift. It returns the new value which is the value returned by running b << a.

Right Shift, shr(a, b)

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function shiftRight(uint256 num, uint256 bitsToShift) public pure returns (bytes32) {
        assembly {
            mstore(0x80, shr(bitsToShift, num))
            return(0x80, 0x20)
        }
    }
}

shr(a, b) is a Yul function that takes in two arguments, a, the number of bits b would be shifted by, and b, the number to shift. It returns the new value which is the value returned by running b >> a.

And, and(a, b)

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function and(uint256 num, uint256 andNum) public pure returns (bytes32) {
        assembly {
            mstore(0x80, and(num, andNum))
            return(0x80, 0x20)
        }
    }
}

and(a, b) is a Yul function that takes in two numbers as arguments. It returns the new value which is the value returned by running a & b.

Or, or(a, b)

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function or(uint256 num, uint256 orNum) public pure returns (bytes32) {
        assembly {
            mstore(0x80, or(num, orNum))
            return(0x80, 0x20)
        }
    }
}

or(a, b) is a Yul function that takes in two numbers as arguments. It returns the new value which is the value returned by running a | b.

Xor, xor(a, b)

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function xor(uint256 num, uint256 xorNum) public pure returns (bytes32) {
        assembly {
            mstore(0x80, xor(num, xorNum))
            return(0x80, 0x20)
        }
    }
}

xor(a, b) is a Yul function that takes in two numbers as arguments. It returns the new value which is the value returned by running a ^ b.

Not, not(a)

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function not(uint256 num) public pure returns (bytes32) {
        assembly {
            mstore(0x80, not(num))
            return(0x80, 0x20)
        }
    }
}

not(a) is a Yul function that takes in a number as argument. It returns the new value which is the value returned by running ~a.

Conditionals


These involve if and switch statements.

There are no else statements in Yul.

if

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Is x greater than or equal to 10?
    function ifElse(uint256 x) public pure returns (bytes32) {
        assembly {
            let res
            if lt(x, 10) { res := 0 }
            if eq(x, 10) { res := 1 }
            if gt(x, 10) { res := 1 }
            
            mstore(0x80, res)
            return(0x80, 0x20)
        }
    }
}

lt(a, b) returns true if a < b. gt(a, b) returns true if a > b. eq(a, b) returns true if a == b.

switch

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Is x greater than or equal to 80?
    function switch(uint256 x) public pure returns (bytes32) {
        assembly {
            let isTrue
            
            switch gt(x, 79)
            case 1 { isTrue := 0x01 }
            default { isTrue := 0x00 }
            
            mstore(0x80, isTrue)
            return(0x80, 0x20)
        }
    }
}

Functions


Functions are declared by the function keyword. And they are of two types, functions with return values and those without.

Functions without a return value.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function fnWithoutReturn(uint256 a, uint256 b) public pure returns (uint256) {
        assembly {
            function sum(num1, num2) {
                mstore(0x80, add(num1, num2))
            }
            
            sum(a, b)
            return(0x80, 0x20)
        }
    }
}

Functions without a return value.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function fnWithReturn(uint256 a, uint256 b) public pure returns (uint256) {
        assembly {
            function sum(num1, num2) -> total {
                total := add(num1, num2)
            }
            
            mstore(0x80, sum(a, b))
            return(0x80, 0x20)
        }
    }
}

Loops


Yul only has for loops. There exists no while loops in Yul.

for Loops

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function forLoop() public pure returns (bytes32) {
        assembly {
            let x := 0
            for { let i := 0 } lt(i, 10) { i := add(i, 1) } {
                x := add(x, 1)
                if eq(x, 5) { continue } // Skip value.

                // This will not run because 5 is skipped.
                if eq(x, 5) { break } // Stop loop.
                
                if eq(x, 10) { break }
            }
            
            mstore(0x80, x)
            return(0x80, 0x20)
        }
    }
}

while Loops

To make a for loop act like a while loop, this is how we do it.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function whileLoop() public pure returns (bytes32) {
        assembly {
            let x := 0
            for { } lt(x, 10) { } {
                x := add(x, 1)
            }
            
            mstore(0x80, x)
            return(0x80, 0x20)
        }
    }
}

Infinite Loops

To achieve the behaviour of an infinite loop, an approach to the for loop is used.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function infiniteLoop() public pure returns (bytes32) {
        assembly {
            let x := 0
            for { } 1 { } {
                x := add(x, 1)
            }
            
            mstore(0x80, x)
            return(0x80, 0x20)
        }
    }
}

Errors


Errors are encoded using the ABI Specification. We will take a look at basic reverts, reverts of errors without parameters and errors with parameters.

Basic Revert

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function basicRevert(uint256 num) public pure {
        assembly {
            if lt(num, 0x06) {
                revert(0x00, 0x00)
            }
        }
    }
}

Revert Without Arguments

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // bytes4(keccak256(NumberLess())) => 0x994823ad.

    function revertWithArgs(uint256 num) public pure {
        assembly {
            if lt(num, 0x06) {
                mstore(0x80, 0x994823ad) // 4 bytes.
                revert(0x9c, 0x04) // Reads 4 bytes.
            }
        }
    }
}

Revert With Arguments

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // bytes4(keccak256(NumberLessThan6(uint256))) => 0x8205edea
    
    function revertWithErrorMessage(uint256 num) public pure {
        assembly {
            if lt(num, 0x06) {
                mstore(0x80, 0x8205edea)
                mstore(0xa0, num)
                revert(0x9c, 0x24)
            }
        }
    }
}

Hash


To hash a part of memory in Yul, we use the keccak256(a, b) function. This function takes in two arguments, the location in memory to start and the size of the memory to hash.

Normal Hash

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function hash(uint256 a) public pure returns (bytes32) {
        assembly {
            mstore(0x80, a)
            let hash := keccak256(0x80, 0x20)
            mstore(0x80, hash)
            return(0x80, 0x20)
        }
    }
}

To Has A Variable In Memory

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function hashString(string memory s) public pure returns (bytes32) {
        assembly {
            mstore(0x80, 0x20)
            mstore(0xa0, mload(s))
            mstore(0xc0, mload(add(s, 0x20)))
            mstore(0x80, keccak256(0x80, 0x60))
            return(0x80, 0x20)
        }
    }
}

Is Contract


This is used to check if an address is the address of a smart contract or an EOA.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function isContract(address _address) public view returns (bool) {
        assembly {
            let size := extcodesize(_address)
            
            switch size
            case 0 { mstore(0x80, 0x00) }
            default { mstore(0x80, 0x01) }
            
            return(0x80, 0x20)
        }
    }
}

The extcodesize(a) function takes in an address and returns the size of the contract code at that address, for EOAs, it's 0, for contracts, it's greater than 0.

In this case, we returned bool and Solidity automatically converted it for us. Had we returned bytes32 like we did below, we would have seen 0x01 for true and 0x00 for false.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function isContract(address _address) public view returns (bytes32) {
        assembly {
            let size := extcodesize(_address)
            
            switch size
            case 0 { mstore(0x80, 0x00) }
            default { mstore(0x80, 0x01) }
            
            return(0x80, 0x20)
        }
    }
}

Ether Balance


This is how you check the Ether balance of an address on any EVM chain.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function getBalance(address _address) public view returns (bytes32) {
        assembly {
            mstore(0x80, balance(_address))
            
            return(0x80, 0x20)
        }
    }
}

The balance(a) function will take in an address and return the Ether balance of that address.

Events


Events are classified as logs on the blockchain. It ranges from log0 to log4.

log[n]

n shows the number of topics in an event. For anonymous events they can have up to 4 indexed event arguments. For the other types of events, the first topic is the event hash, a 32 byte value of the hashed event signature. The subsequent topics are indexed event arguments.

If your anonymous event has x indexed arguments where 0 <= x <= 4, the corresponding log[n] to be used would be log[x].

If your non-anonymous event has x indexed arguments where 0 <= x <= 3, the corresponding log[n] to be used would be log[x + 1].

The log[n] for a non-anonymous event takes in a range of 2 to 6 arguments, as defined by n + 2, the first two being the part of the calldata where non-indexed values are stored according to the ABI Specification. Then the number of indexed events as much as n.

If we have a non-anonymous event with 2 indexed arguments, we will use log3, and log3 would take in 5 arguments.

These five arguments would be log2(a, b, c, d, e), a, the offset, the part of memory where the stored non-indexed values start, b, the size of the memory to be read, c, the event topic, d, the value of the first indexed argument, e, the value of the second indexed argument.

If we have an anonymous event with 2 indexed arguments, we will use log2, and log2 would take in 4 arguments.

These four arguments would be log2(a, b, c, d), a, the offset, the part of memory where the stored non-indexed values start, b, the size of the memory to be read, c, the value of the first indexed argument, d, the value of the second indexed argument.

anonymous events without indexed arguments are emitted using log0.

indexed Event

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Event to emit.
    event IndexedEvent(address indexed a, uint256 indexed b);

    bytes32 public constant IndexedEventTopic = 0xfdcfbb6e25802e9ba4d7c62d4a85a10e40219c69383d35be084b401980dd7156;

    function emitIndexedEvent(uint256 x) public {
        assembly {
            log3(0x00, 0x00, IndexedEventTopic, caller(), x)
        }
    }
}

Non-indexed Event

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Event to emit.
    event NonIndexedEvent(uint256 a, uint256 b);

    bytes32 public constant NonIndexedEventTopic = 0x8a2dba84c9d33350ff3006c2607aae76d062ae5ac6632d800030613bcf7f74a0;
    
    function emitNonIndexedEvent(uint256 a, uint256 b) public {
        assembly {
            mstore(0x80, a)
            mstore(0xa0, b)
            log1(0x80, 0x40, NonIndexedEventTopic)
        }
    }
}

anonymous indexed Events

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Event to emit.
    event AnonymousIndexedEvent(address indexed a, uint256 indexed b) anonymous;

    function emitAnonymousIndexedEvent(uint256 x) public {
        assembly {
            log3(0x00, 0x00, caller(), x)
        }
    }
}

anonymous non-indexed Events

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    // Event to emit.
    event AnonymousNonIndexedEvent(address a, uint256 b) anonymous;
    
    function emitAnonymousNonIndexedEvent(uint256 a, uint256 b) public {
        assembly {
            mstore(0x80, a)
            mstore(0xa0, b)
            log1(0x80, 0x40)
        }
    }
}

Send Ether


Sends a specified msg.value to an address using call().

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    receive () external payable {}
    
    function sendEther(uint256 amount, address to) external {
        assembly {
            let s := call(gas(), to, amount, 0x00, 0x00, 0x00, 0x00)
            
            if iszero(s) {
                revert(0x00, 0x00)
            }
        }
    }
}

call(a, b, c, d, e, f, g) takes in 7 arguments:

a - The gas. Gotten by calling the gas() function in Yul.
b - Recipient, an address.
c - msg.value, amount to send.
d - offset, for calldata.
e - size, for calldata.
f - Return data offset.
g - Return data size.

It returns true or false depending on the success of the transaction.

delegatecall(a, b, d, e, f, g) and staticcall(a, b, d, e, f, g) take exactly the same as call(a, b, c, d, e, f, g) except the msg.value (c) argument.

They all return the same thing, true or false.

Check them out at evm.codes at call, delegatecall and staticcall.

Signature Verification


Source: Verifying Signature, solidity-by-example.org [7].

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Yul {
    function splitSignature(bytes memory signature) public pure returns (
        bytes32 r,
        bytes32 s,
        uint8 v
    ) {
        // Signatures are 65 bytes in length. r => 32, s => 32, v => 1.
        // Although some signatures can be packed into 64 bytes.
        if (signature.length != 65) revert();

        assembly {
            r := mload(add(signature, 0x20))
            s := mload(add(signature, 0x40))
            v := byte(0, mload(add(signature, 0x60)))
        }
    }

    function verifySignature(
        address signer,
        bytes32 hash,
        bytes memory sig
    ) public pure returns (bool) {
        (bytes32 r, bytes32 s, uint8 v) = splitSignature(sig);
        address recovered = ecrecover(hash, v, r, s);

        return (recovered != address(0) && recovered == signer);
    }
}

byte(a, b) retrieves a single byte at position a in a 32-byte memory word, b. In our case, we are simply saying "Return the first byte in the 32-byte location at add(signature, 0x60)."

ecrecover(a, b, c, d) returns the address of a signer when given four arguments, a, the hash that was signed and b, c, d being the v, r and s components of the signature, respectively.

call


call(a, b, c, d, e, f, g) is used to send Ether to an EOA or a smart contract, and also, send a specified calldata alongside the transaction to be executed when the smart contract receives the call.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract CalledContract {
    uint256 public storedNumber;
    
    receive () external payable {}

    function setNumber(uint256 num) external {
        assembly {
            sstore(0x00, num)
        }
    }
    
    function setNumberWithEther(uint256 num) external payable {
        assembly {
            if eq(callvalue(), 0) { revert(0x00, 0x00) }
            sstore(0x00, num)
        }
    }
}

contract CallerContract {
    address public calledContract;

    receive () external payable {}
    
    // Deploy with address of CalledContract.
    constructor(address _address) {
        assembly {
            sstore(0x00, _address)
        }
    }

    function callContract(uint256 num) public {
        assembly {
            // Start at 0x1c, this is the first calldata entry.
            // Call setNumber(uint256).
            mstore(0x00, 0x3fb5c1cb) 
            mstore(0x20, num)

            // To learn about calldata encoding: https://rb.gy/vmzhck.
            // Read 32 + 4 bytes.
            let success := call(gas(), sload(0x00), 0, 0x1c, 0x24, 0, 0)

            if iszero(success) { revert(0x00, 0x00) }
            // In Called.sol, number == num.
        }
    }

    function callContractWithEther(uint256 num) public payable {
        assembly {
            // Start at 0x1c, this is the first calldata entry.
            // Call setNumberWithEther(uint256).
            mstore(0x00, 0xcc95ae02)
            mstore(0x20, num)

            // To learn about calldata encoding: https://rb.gy/vmzhck.
            // Read 32 + 4 bytes.
            let success := call(gas(), sload(0x00), callvalue(), 0x1c, 0x24, 0, 0)

            if iszero(success) { revert(0x00, 0x00) }
            // In Called.sol, number == num.
        }
    }
}

callvalue() is used to retrieve msg.value in Yul.

staticcall


The difference between staticcall and delegatecall is that staticcall makes calls to only view or pure functions. A staticcall will revert if it makes a call to a function in a smart contract that changes the state of that smart contract.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract CalledContract {
    struct Data {
        uint128 x;
        uint128 y;
    }

    string public myString;
    uint256 public bigSum;

    fallback() external {}

    function add(uint256 i) public pure returns (uint256) {
        return i + 78;
    }

    function multiply(uint8 i, uint8 j) public pure returns (uint256) {
        return uint256(i * j);
    }

    function arraySum(uint256[] calldata arr) public pure returns (uint256) {
        uint256 len = arr.length;
        uint256 sum;

        for (uint256 i; i != len; i++ ) {
            sum = sum + arr[i];
        }

        return sum;
    }

    function setString(string calldata str) public {
        if (bytes(str).length > 31) revert();
        myString = str;
    }

    function structCall(Data memory data) public {
        bigSum = uint256(data.x + data.y);
    }
}

contract CallerContract {
    address public _calledContract;
    
    constructor() {
        _calledContract = address(new CalledContract());
    }

    /// @notice add: 1003e2d2.
    function callAdd(uint256 num) public view returns (uint256) {
        address calledContract = _calledContract;

        assembly {
            mstore(0x80, 0x1003e2d2)
            mstore(0xa0, num)

            let success := staticcall(gas(), calledContract, 0x9c, 0x24, 0x00, 0x00)

            if iszero(success) {
                revert (0x00, 0x00)
            }

            returndatacopy(0x80, 0x00, returndatasize())
            return(0x80, returndatasize())
        }
    }

    /// @notice multiply: 6a7a8e0b.
    function callMultiply(uint8 num1, uint8 num2) public view returns (uint256) {
        address calledContract = _calledContract;

        assembly {
            mstore(0x80, 0x6a7a8e0b)
            mstore(0xa0, num1)
            mstore(0xc0, num2)

            let success := staticcall(gas(), calledContract, 0x9c, 0x44, 0x00, 0x00)

            if iszero(success) {
                revert (0x00, 0x00)
            }

            returndatacopy(0x80, 0x00, returndatasize())
            return(0x80, returndatasize())
        }
    }

    /// @notice arraySum: 7c2b11cd.
    function callArraySum(
        uint256 num1,
        uint256 num2,
        uint256 num3,
        uint256 num4
    ) public view returns (uint256)
    {
        address calledContract = _calledContract;

        assembly {
            mstore(0x80, 0x7c2b11cd)
            mstore(0xa0, 0x20)
            mstore(0xc0, 0x04)
            mstore(0xe0, num1)
            mstore(0x0100, num2)
            mstore(0x0120, num3)
            mstore(0x0140, num4)

            let success := staticcall(gas(), calledContract, 0x9c, 0xc4, 0x00, 0x00)

            if iszero(success) {
                revert (0x00, 0x00)
            }

            returndatacopy(0x80, 0x00, returndatasize())
            return(0x80, returndatasize())
        }
    }

    /// @notice setString: 7fcaf666.
    function callSetString(string calldata str) public {
        uint8 len = uint8(bytes(str).length);
        if (len > 31) revert();

        address calledContract = _calledContract;
        bytes memory strCopy = bytes(str);

        assembly {
            mstore(0x0200, mload(add(strCopy, 0x20)))

            mstore(0x80, 0x7fcaf666)
            mstore(0xa0, 0x20)
            mstore(0xc0, len)
            mstore(0xe0, mload(0x0200))

            let success := call(gas(), calledContract, 0, 0x9c, 0x64, 0x00, 0x00)
        }
    }

    function getNewString() public view returns (string memory) {
        return CalledContract(_calledContract).myString();
    }

    function callStructCall(uint128 num1, uint128 num2) public {
        address calledContract = _calledContract;
        bytes4 _selector = CalledContract.structCall.selector;

        assembly {
            mstore(0x9c, _selector)
            mstore(0xa0, num1)
            mstore(0xc0, num2)

            let success := call(gas(), calledContract, 0, 0x9c, 0x44, 0x00, 0x00)
        }
    }

    function getBigSum() public view returns (uint256) {
        return CalledContract(_calledContract).bigSum();
    }
}

returndatacopy(a, b, c) copies the data returned from a function call to location a in memory. But that's not all, it tells the EVM to copy starting from b and copy as much as c in bytes. In other words, "Start from position b in the returned data and copy c bytes of the returned data to memory location a.".

returndatasize() returns the size of the data returned from a function call.

delegatecall


delegatecall(a, b, d, e, f, g) functions exactly the way staticcall() functions in Solidity. It takes the same arguments call takes except the msg.value.

It is called exactly the same way staticcall is called, with the same arguments, and returns the same value as well.

Sources And References


  1. Yul (https://docs.soliditylang.org/en/latest/yul.html#yul).
  2. What is Yul? (https://www.quicknode.com/guides/ethereum-development/smart-contracts/what-is-yul#what-is-yul).
  3. Solidity Docs - Layout In Memory (https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html).
  4. Solidity Docs - bytes and string (https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#bytes-and-string).
  5. Solidity Docs - User Defined Value Types (https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types).
  6. OpenZeppelin Safe Math (https://github.com/ConsenSysMesh/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol).
  7. Solidity By Example (https://solidity-by-example.org/signature/).




Christ is King!