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
-
Perelyn, (@okohebina).
-
fps, (@0xfps).
-
Aryan Malik, (@theAryanMalikX).
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] type | Minimum Value | Maximum Value |
---|---|---|
int8 | -128 | 127 |
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.
Value | Value | Value | Value | Value | Value | |
---|---|---|---|---|---|---|
0 | 1 | 2 | ... | 125 | 126 | 127 |
0x00 | 0x01 | 0x02 | ... | 0x7d | 0x7e | 0x7f |
-1 | -2 | -3 | ... | 126 | 127 | -128 |
0xff | 0xfe | 0xfd | ... | 0x82 | 0x81 | 0x80 |
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 auint[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 inuint
'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 that0xab
is abytes
with a length of1
, and0xabcd
is abytes
with a length of2
.
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 last00
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
into0x0123456789abcdef0000000000000000000000000000000000000000000000
(31 bytes) and10
(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
andstring
share the same characteristics in storage and memory. The same waybytes
are stored in storage and memory, that is the same waystring
are stored. The only differences are that eachstring
character is converted into their hexadecimal components before storage. And that whilebytes
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 tomsg.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
andstring
.
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 auint[n]
type overflow, meaning that-1
will be converted to(2^256) - 1
. This can lead to security breaches. Yul wraps around negativeint[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 aconstant
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.
- The pointer.
- The length.
- 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
- The pointer.
- The length.
- 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.
- The pointer.
- The length.
- 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 the0
when subtracting, that is ifb > 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 0
meaning 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
0
meaning 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
- Yul (https://docs.soliditylang.org/en/latest/yul.html#yul).
- What is Yul? (https://www.quicknode.com/guides/ethereum-development/smart-contracts/what-is-yul#what-is-yul).
- Solidity Docs - Layout In Memory (https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html).
- Solidity Docs - bytes and string (https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#bytes-and-string).
- Solidity Docs - User Defined Value Types (https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types).
- OpenZeppelin Safe Math (https://github.com/ConsenSysMesh/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol).
- Solidity By Example (https://solidity-by-example.org/signature/).
Christ is King!