Assembly has been really hyped lately and all the cool kids seem to have started learning it. So I decided to do the same and learn what Yul is and how I can write my contracts with it. I started learning a few weeks ago and today I’m going to show you the basics so you can start enjoying it.

This tutorial is a bit advanced and doesn’t start from the beginning. If you want a real intro, you can check out the amazing series by @noxx3xxon EVM Deep Dives: The Path to Shadowy Super Coder where everything is really well explained. Or this great tutorial by @JeanCavallera.

What is Assembly/Yul

I’m not going to go into too much details, but basically assembly (or assembler) is a low-level language, really close to what your computer can understand. It’s a sequence of instructions for your computer to execute (here: the EVM).

Yul is just the name of the (almost) assembly for the EVM. I say “almost” because it’s a bit easier to write than pure assembly and it has the concept of variables, functions, for-loops, if statements, … whereas pure assembly doesn’t. So Yul makes our lives a bit easier 😊

You can use Yul when you need to have more control over what your code is doing. You can do anything in Yul since you control exactly what the EVM is going to execute, while Solidity is more restrictive. And most of the time Yul is used for gas optimizations.

Writing an entire contract in assembly (Yul) wouldn’t make sense usually, but that’s what we are going to do here so that you can understand better how it works and how the EVM works.

The base contract

As I already said, I’ll try to explain as much as possible, but I will not go over the very basics. So if you want to understand this tutorial, you’ll need a good understanding of Solidity and the EVM.

Let’s begin! We’ll rewrite this (really unsafe and stupid 😄) “lottery” contract to assembly. No access control, and the functions are a bit dumb, but it will be easier to write/understand when written in assembly 😊

contract AngleExplainsBase {
    uint private secretNumber;
    mapping(address =>  uint) public guesses;
    
    bytes32 public secretWord;

    // obviously this doesn't make sense
    // but it will be fun to write it in assembly :D
    function getSecretNumber() external view returns(uint) {
        return secretNumber;
    }

    // this should only be set by an admin
    // no access control because we want to keep it simple in assembly
    function setSecretNumber(uint number) external {
        secretNumber = number;
    }

    // a user can add a guess
    function addGuess(uint _guess) external {
        guesses[msg.sender] = _guess;
    }

    // yes i know... it doesn't make sense because you can change guesses for any user
    // it's just to teach you how to parse arrays in assembly
    function addMultipleGuesses(address[] memory _users, uint[] memory _guesses) external {
        for (uint i = 0; i < _users.length; i++) {
            guesses[_users[i]] = _guesses[i];
        }
    }

    // this is useless since the `secretWord` is not used anywhere
    // but this will teach us how to hash a string in assembly. Really cool! :)
    function hashSecretWord(string memory _str) external {
        secretWord = keccak256(abi.encodePacked(_str));
    }
}

We have a secretNumber that is private and we have a getter for that secret number getSecretNumber. Yes, it doesn't make, but if you're reading this, you should know that nothing is private on the blockchain anyway, so it doesn't really matter if we add a getter. It will just be fun to write it to assembly. Then, obviously, we have a setter setSecretNumber.

The user can add 1 or multiple guess(es) addGuess / addMultipleGuesses.

Then we have an extra function hashSecretWord. Let's imagine, we could use it if we decide to switch from a secret number to a secret string. Here it will help us understand more about how strings are handled in memory and we'll learn how to hash something in assembly (so cool!).

Get/Set our secret number

Throughout the code you’ll see a lot 0x20 or multiples of it (0x40, 0x60, 0x80, 0xa0, ...). This is the hexadecimal representation of 32 because the EVM uses 32 bytes memory slots (words). So values are always encoded in 32 bytes. (yes, you should know how to count in hexadecimal).

Let’s start with getSecretNumber. We will need SLOAD, MLOAD, MSTORE and RETURN opcodes for this function. Use the great https://www.evm.codes to learn more about EVM opcodes.

SLOAD just retrieves a value from storage. So we use SLOAD to get the value of our secret number. Then, in an ideal world, we should be done and just be able to return that number. But the EVM is a bit more complex than that and only returns values that are stored in memory. Solidity makes it easier for us and allows us to just return a value, and we don’t care what happens under the hood, but remember that Yul is more lower level, so we need to do the hard work.

First we’ll store that value to memory with MSTORE, and then we can return it.

Free memory pointer

We’ll use the “free memory pointer”, which is stored at 0x40 in memory.

mload(0x40) gives us the address in memory where we are allowed to write (in order not to overwrite anything). The memory before that address is already used, so if we overwrite it we might mess up our entire transaction (or even contract 😮 if something in that memory was meant to be written to storage for example).