Advanced Smart Contract Development for Celo Developers: Working with Yul and Inline Assembly in Solidity - Part 1

Advanced Smart Contract Development for Celo Developers: Working with Yul and Inline Assembly in Solidity - Part 1 https://celo.academy/uploads/default/optimized/2X/3/3ad3e6b730e75e93af4e418b99dbce805e13040e_2_1024x576.png
none 0.0 0

Introduction

There are disparities in how we code or write programs. Often, our code is who we are and how well we are able to manipulate our thoughts. This means that the quality of code we present largely depends on our level of skills. The difference between the two solidity developers is how well they can decipher and simplify complexities in programs. A quote by @Mudit_Gupta reads: "Unironically, the things that often separates Senior Solidity devs from Solidity devs is their ability to write “not” in their Solidity smart contracts. Solidity lets us write instructions (in high-level form) for the VM to execute. One of the disadvantages of this is that we are not able to write expressions in a more native form that gives us good control of the VM for optimization.

Prerequisites​

This is an advanced smart contract development tutorial hence, I assume you have at least intermediate experience writing smart contracts. If you’re new in this field, I recommend starting from here and here.
I also assume you know how to deploy contracts to the Celo networks. For references, please check the below tutorials.

Requirements​

For this tutorial, you do not need to install any library or CLI tool. We’ll be using an online Integrated Development Environment (IDE) known as Remix

Writing code in machine’s native language or dialect gives more control at the low level but with complexities involved and the syntaxes used are referred to as assembly code. For us to get a good grasp of Yul, it is important to understand the EVM including some of its components.

Overview of the EVM:

The stack

The EVM is a stack machine much like a box and can be viewed as a rectangular-shaped object with width and depth. The width equivalent to the length of an item in the stack is 256 bits i.e. 32 bytes. A stack is restricted to a depth of 1024 often called call depth, i.e. the number of items that can be arranged in a stack on top of one another cannot exceed 1024. The stack is more like an area of operation. An example is a function in a contract. A function is like an enclosed area where operations are performed.

stack

Ex: If you declare a few expressions thus:

contract StackExample {
   function doSomething() public return(unit _something) {
       uint a = 1 ether;
       bytes empty = bytes("");
   }
}

Considering the function above, the area where the variables a and empty are placed can be termed stack. Each of these variables is an operation. In a stack, you can access and manipulate up to 16 items at a time otherwise you get a stack too deep error because some variables you declared in a function have been pushed down the stack as you declare more variables. You need to bring them forward so they can be accessed but you have to know what you’re doing.

image

There are ways to work around the stack too deep error. This is not a topic for today. I will make a separate post on the stack as it is a bit wide.

Memory

Memory is a byte-array or an array of bytes. Memories are temporary slots that do not persist in the program only accessible within the current executing window. It starts from zero index and can be expanded in 32-byte chunks by simply adding more items at the indices greater than the current size of the array.

memory

Storage

Storage can be perceived as the unit of database assigned for smart contracts. In contrast to Memory, Storage persists data across functions. It holds the state variables of the contract and can be costlier to use than memory but the benefit is worth it. The below image is an account storage represented in key-value mapping.

image

Before we proceed to the inline assembly code, let’s quickly see what EVM codes are. They’re an integral part of understanding the inline assembly and Yul. Check this site for more information on EVM codes and what they represent.

Inline Assembly

Assembly in Solidity is an intermediate language for the compiler that converts the Solidity code into an assembly and then to byte code. Inline assembly code can be interleaved within the Solidity code base to have more fine-grain control over EVM. It is used mostly while writing library functions.

An assembly code is written using the assembly keyword. followed by the curly braces {}.

assembly { ... }

Example:
The below code shows how to use inline assembly code in Solidity. We’ll get into detail in part 2 of this tutorial.

// SPDX-License_Identifier: MIT
contracts AllocateMemory {
  assembly {
      function allocateMemory(length) -> pos {
          index := mload(0x40)
          mstore(0x40, add(index, length))
      }
      let free_memory_pointer := allocate(64)
  }
}

Introduction to Yul - A high-level optimization language

Now that we have a refreshed memory of the EVM, let’s take a look at what Yul looks like. Solidity defined Yul as an intermediate language that can be compiled to bytecode for different backends. It can be used in stand-alone mode and for “inline assembly” inside Solidity. For high-level optimization stages that can benefit all target platforms equally, Yul is a good target.

What Yul tries to achieve

Yul is targeted to achieve the following set of goals:

  • Programs written in Yul should be readable, even if the code is generated by a compiler from Solidity or another high-level language.

  • Control flow should be easy to understand to help in manual inspection, formal verification, and optimization.

  • The translation from Yul to bytecode should be as straightforward as possible.

  • Yul should be suitable for whole-program optimization.

  • To compile the higher-level constructs to bytecode in a very regular way.

One of the benefits of using Yul s that Even though it was targeted at stack machines, It does not expose the complexity of the stack itself. So as the programmer or auditor, you do not have to worry about the stack.

Note: In order to avoid confusion between concepts such as values and references, Yul is statically typed. At the same time, there is a default type (usually the integer word of the target machine) that can always be omitted to help readability.

Yul’s Syntax

  • Comments

    Yul parses comments, identifiers, and literals in ways similar to Solidity.
    Use // and /* */ to denote comments.
    An exception in Yul is that identifiers can contain dots: ..

  • Code block

    Yul delimits a block using curly braces {}

    • declaring a variable,

         let y := 5,  
         let x := add(y, 3) 
         let x  - initial value will be set to zero (0)
      
    • literals (string or hex or integer literals.

      E.g 0x1234, 47 or "abc" (strings up to 32 characters)

    • Calling builtin functions:

          add(1, mload(0))
          sub(6, mload(0))
          sub(6,  add(1, mload(0)))
      
    • identifiers (variables)

          add(5, y)
          sub(4, x)
          mul(7, z)
      
    • assignments,

         x := add(y, 6)
         y := 5
         x := div(z, 2)
      
    • Local block scope i.e. where local variables are scoped

       { 
           let initValue := 3 
           { 
              let result := add(initValue, 10) 
           } 
       }
      
    • if statements

         if lt(x, y) { 
              sstore(0, 1) 
         }
      
    • Switch statements

      {
          function power(_b, _exp) -> result
          {
              switch _exp
              case 0 { result := 1 }
              case 1 { result := _b }
              default
              {
                  result := power(mul(_b, _b), div(_exp, 2))
                  switch mod(_exp, 2)
                      case 1 { result := mul(_b, result) }
              }
          }
      }
      
    • for loops

         for { let i := 0} lt(i, 10) { i := add(i, 1) } { 
           mstore(i, 7) 
         }
      
    • Defining functions
      The function accepts two inputs a and b and return an output c.

          function doSomething(a, b) -> c { 
              c := add(a, b) 
         }
      

Conclusion​

Congratulations on completing the first. This is the beginning of your journey to becoming an advanced smart contract developer. This is only a part of the tutorial. I will complete and link to part 2 here very soon.

What next?

To learn how to deploy your dream project on the Celo blockchain, visit the Celo Academy

About the Author​

Isaac Jesse, aka Bobelr is an advanced smart contract/Web3 developer. He has been in the field since 2018. He’s been an ambassador for projects such as Algorand, AtomicWallet, DxChain, etc. as a content creator and developer ambassador. He has also contributed to Web3 projects.

References​

17 Likes

Fantastic news! Your proposal has landed in this week’s top voted list. As you begin your project journey, remember to align with our community and technical guidelines, ensuring a high quality platform for our developers. Congratulations! :mortar_board: :seedling:

Note: @bobelr lets find a way to break this down and focus on a tutorial from the program. We cannot approve the entirety of this at once, but given the interest it would be nice to organize what we can to help put this together and get started on a tutorial. Thanks!

5 Likes

Ok great. Thank you @Celo_Academy I’ll do my best to sort it. Perhaps, I’ll filter out one of the most pressing to fit the need of the community.

Thanks again for doing great job.

11 Likes

Bob, I thought this post was supposed to be a full course? I was anticipating it to help me have a clear path to my developer journey. But I noticed the title and content has changed. Are you creating a new one?

4 Likes

Yes you’re right bro. I’m still hoping the course is approved. For now, just one of the paths was approved so I had to edit the title.

10 Likes

@bobelr i’ll be reviewing this

3 Likes

Ok @4undRaiser I’ll be expecting asap. Thanks

10 Likes