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.
-
How to Deploy your Smart Contract to Celo using Java and Web3js by @Phenzic
-
Deploying Smart Contracts on Celo Using Foundry - A Step-by-Step Guide by @bobelr
-
How to Deploy a Smart Contract to the Celo Testnet using Hardhat by @oyeniyiabiola98
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.
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.
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.
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.
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 inputsa
andb
and return an outputc
.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.