Skip to main content

What is Yul?

Created on
Updated on
Dec 17, 2024

33 min read

Overview

Solidity has been the dominant language for writing smart contracts on Ethereum, but as the ecosystem continues to grow and mature, developers are exploring new ways to optimize and customize their code. Yul is a low-level language that provides a more granular level of control over smart contract execution, making it an ideal choice for developers who need to squeeze every last bit of efficiency out of their code.

What You Will Need

  • Basic understanding of Ethereum and Solidity (you can get started here)
  • A desire to learn!

What You Will Do

  • Learn about Yul
  • Dive deeper into the Yul Syntax
  • Compare Yul and Solidity

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.

Yul Syntax

Data Types

Yul includes several built-in data types that are used to store and manipulate values. Here are a few of the most common data types in Yul:

Integers: Integers are used to represent whole numbers. In Yul, integers can be signed or unsigned, and can have different bit sizes. For example, a uint256 is an unsigned integer with 256 bits. Byte arrays: Byte arrays are used to store sequences of bytes. They are often used to represent data such as hashes or public keys. In Yul, byte arrays are declared using the bytes keyword, followed by the number of bytes to be allocated. Structs: Structs are used to group related data together. They are similar to objects in other programming languages. In Yul, structs are declared using the struct keyword, followed by the names and types of each field in the struct. Here's an example of how you might declare and use a struct in Yul:

struct Person {
string name;
uint age;
}

Variables and Constants

Variables and constants are used to store and manipulate data in Yul. Here's a quick overview of how to declare and use variables and constants in Yul:

Variables: Variables are declared using the let keyword, followed by the variable name and value. Here's an example of how to declare a variable in Yul:

let x = 100
let x //initial value of 0 is assigned

Operators

Operators in Yul are used to perform arithmetic and logical operations on values. Here are some of the most common operators in Yul:

Addition (+): Adds two values together. For example:

add(x,y) // x + y

Subtraction (-): Subtracts one value from another. For example:

sub(x,y) // x - y

Multiplication (*): Multiplies two values together. For example:

mul(x,y) // x * y

Division (/): Divides one value by another. For example:

div(x,y) //x / y

Modulus (%): Computes the remainder of a division operation. For example:

mod(x,y) //x % y

Equivalent: Checks that the value is equivalent. For example:

x := y // x = y

Control Flow Statements

Control flow statements in Yul are used to control the flow of execution within a contract. Here are some of the most common control flow statements in Yul. Note that an “else” block cannot be defined. Consider using “switch” instead (see below) if you need multiple alternatives.

If Statement:

if lt(calldatasize(), 4) { revert(0, 0) }

Switch Statement:

{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}

For Loop:

// Simple for loop
for {let i := 0} lt(i, 10) {i := add(i, 10)} {
let p := funcCall(i, 10)
if eq(p, 5) {
continue
}

if eq(p, 90) {
break
}
}

Functions

Functions in Yul are used to encapsulate blocks of code that can be executed at a later time. Here's an example of how to declare and use a function in Yul:

function sum(a, b) -> ret : u64
{
ret := add(b, a)
}

Note that Yul functions are limited to operating only with the variables passed to them as arguments, as they do not have access to any variable outside their scope. It is possible to specify the types of the arguments and returns, and if no type is specified, the u256 type is used as a default by the compiler.

Assembly

Yul also includes an inline assembly language that allows you to write low-level code directly in your Yul contracts. This can be useful for optimizing certain operations or interacting with the Ethereum Virtual Machine (EVM) directly. Here's an example of how to use inline assembly in Yul:

assembly {
let x := 0
let y := 1
add(x, y)
}

Other built-in functions include:

  • calldata: A special data location in Yul that refers to the input data that is passed to a function or contract call. It is read-only, meaning that you cannot modify the calldata directly. Instead, you can copy data from calldata to other data locations, such as memory or storage, using the calldataload opcode.
  • revert: A low-level function in Yul used to abort the execution of a transaction and revert any state changes that have been made. It is commonly used to handle error conditions in smart contracts. When revert is called, any gas that has not been used is refunded to the caller. The function takes an optional argument, which is a message that can be returned to the caller to provide additional information about the error.

The remaining list can be found here. Additionally, comments can be used the same in Yul as Solidity, you can use // and /* */ to denote comments, however, one exception is that Identifiers in Yul can contain dots (e.g., .).

For more information on Yul syntax, check out the documentation.

Yul Examples

Yul code is written in a text editor and compiled using the solc Solidity compiler. Yul code consists of a series of instructions, each performing a specific operation on the EVM. These operations include arithmetic and logical operations, memory management, and control flow instructions.

Here's an example of an ERC-20 token smart contract (source), written in Yul and Solidity:

object "Token" {
code {
// Store the creator in slot zero.
sstore(0, caller())

// Deploy the contract
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// Protection against sending Ether
require(iszero(callvalue()))

// Dispatcher
switch selector()
case 0x70a08231 /* "balanceOf(address)" */ {
returnUint(balanceOf(decodeAsAddress(0)))
}
case 0x18160ddd /* "totalSupply()" */ {
returnUint(totalSupply())
}
case 0xa9059cbb /* "transfer(address,uint256)" */ {
transfer(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0x23b872dd /* "transferFrom(address,address,uint256)" */ {
transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2))
returnTrue()
}
case 0x095ea7b3 /* "approve(address,uint256)" */ {
approve(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0xdd62ed3e /* "allowance(address,address)" */ {
returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1)))
}
case 0x40c10f19 /* "mint(address,uint256)" */ {
mint(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
default {
revert(0, 0)
}

function mint(account, amount) {
require(calledByOwner())

mintTokens(amount)
addToBalance(account, amount)
emitTransfer(0, account, amount)
}
function transfer(to, amount) {
executeTransfer(caller(), to, amount)
}
function approve(spender, amount) {
revertIfZeroAddress(spender)
setAllowance(caller(), spender, amount)
emitApproval(caller(), spender, amount)
}
function transferFrom(from, to, amount) {
decreaseAllowanceBy(from, caller(), amount)
executeTransfer(from, to, amount)
}

function executeTransfer(from, to, amount) {
revertIfZeroAddress(to)
deductFromBalance(from, amount)
addToBalance(to, amount)
emitTransfer(from, to, amount)
}


/* ---------- calldata decoding functions ----------- */
function selector() -> s {
s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000)
}

function decodeAsAddress(offset) -> v {
v := decodeAsUint(offset)
if iszero(iszero(and(v, not(0xffffffffffffffffffffffffffffffffffffffff)))) {
revert(0, 0)
}
}
function decodeAsUint(offset) -> v {
let pos := add(4, mul(offset, 0x20))
if lt(calldatasize(), add(pos, 0x20)) {
revert(0, 0)
}
v := calldataload(pos)
}
/* ---------- calldata encoding functions ---------- */
function returnUint(v) {
mstore(0, v)
return(0, 0x20)
}
function returnTrue() {
returnUint(1)
}

/* -------- events ---------- */
function emitTransfer(from, to, amount) {
let signatureHash := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
emitEvent(signatureHash, from, to, amount)
}
function emitApproval(from, spender, amount) {
let signatureHash := 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
emitEvent(signatureHash, from, spender, amount)
}
function emitEvent(signatureHash, indexed1, indexed2, nonIndexed) {
mstore(0, nonIndexed)
log3(0, 0x20, signatureHash, indexed1, indexed2)
}

/* -------- storage layout ---------- */
function ownerPos() -> p { p := 0 }
function totalSupplyPos() -> p { p := 1 }
function accountToStorageOffset(account) -> offset {
offset := add(0x1000, account)
}
function allowanceStorageOffset(account, spender) -> offset {
offset := accountToStorageOffset(account)
mstore(0, offset)
mstore(0x20, spender)
offset := keccak256(0, 0x40)
}

/* -------- storage access ---------- */
function owner() -> o {
o := sload(ownerPos())
}
function totalSupply() -> supply {
supply := sload(totalSupplyPos())
}
function mintTokens(amount) {
sstore(totalSupplyPos(), safeAdd(totalSupply(), amount))
}
function balanceOf(account) -> bal {
bal := sload(accountToStorageOffset(account))
}
function addToBalance(account, amount) {
let offset := accountToStorageOffset(account)
sstore(offset, safeAdd(sload(offset), amount))
}
function deductFromBalance(account, amount) {
let offset := accountToStorageOffset(account)
let bal := sload(offset)
require(lte(amount, bal))
sstore(offset, sub(bal, amount))
}
function allowance(account, spender) -> amount {
amount := sload(allowanceStorageOffset(account, spender))
}
function setAllowance(account, spender, amount) {
sstore(allowanceStorageOffset(account, spender), amount)
}
function decreaseAllowanceBy(account, spender, amount) {
let offset := allowanceStorageOffset(account, spender)
let currentAllowance := sload(offset)
require(lte(amount, currentAllowance))
sstore(offset, sub(currentAllowance, amount))
}

/* ---------- utility functions ---------- */
function lte(a, b) -> r {
r := iszero(gt(a, b))
}
function gte(a, b) -> r {
r := iszero(lt(a, b))
}
function safeAdd(a, b) -> r {
r := add(a, b)
if or(lt(r, a), lt(r, b)) { revert(0, 0) }
}
function calledByOwner() -> cbo {
cbo := eq(owner(), caller())
}
function revertIfZeroAddress(addr) {
require(addr)
}
function require(condition) {
if iszero(condition) { revert(0, 0) }
}
}
}
}

Take a few minutes to review the code above. A lot of the syntax we covered in the Yul Syntax section is implemented in the code above. You can also toggle to the ERC-20 in Solidity tab to see how this contract is written in Solidity.

Yul vs. Solidity

Yul is not a replacement for Solidity but rather a complementary language that can be used in conjunction with it. Yul is much lower-level than Solidity and is designed to provide more direct control over the execution of smart contracts. This can make Yul code more efficient than equivalent Solidity code but also more difficult to write and understand.

Conversely, Solidity is a higher-level language that provides more abstraction and easier-to-use features for developers. It is designed to be more accessible to developers unfamiliar with low-level programming concepts.

Yul Tools and Resources

Many resources are available online if you're interested in learning more about Yul and how to use it in your smart contract development. The Solidity documentation includes a section on Yul and a Yul tutorial. Additionally, many blog posts, articles, and videos cover Yul in more detail.

Several tools are available for working with Yul, such as:

  • Remix.IDE: For compiling and deploying smart contracts written in Yul
  • Hardhat: Plug-ins developing smart contracts with Yul and/or Yul+
  • Solc compiler: Compile Yul code into bytecode

Final Thoughts

Yul is a powerful tool for smart contract developers who need to optimize their code for efficiency and performance. While it may not be suitable for all developers, those who are comfortable with low-level programming concepts and want to fine-tune their code will find Yul to be a valuable addition to their toolkit. We hope this guide has been helpful in introducing you to Yul and its capabilities, and we encourage you to explore further and see what Yul can do for you!

If you're having trouble, have questions, or just want to talk about what you're building, drop us a line on Discord or Twitter!

We ❤️ Feedback!

If you have any feedback on this guide, let us know. We'd love to hear from you!

Share this guide