Write Your First Smart Contract Using Opcodes: A Comprehensive Guide

Pari Tomar
5 min readAug 8, 2023

Smart contracts are the building blocks of blockchain technology. While many developers use popular languages like Solidity, there’s another way to write smart contracts: using opcodes. In this beginner-friendly guide, we’ll explore how to write, test, and deploy a smart contract using only opcodes.

What Are Opcodes and Why Use Them?

Opcodes, short for operation codes, are the basic instructions that the Ethereum Virtual Machine (EVM) understands. They might seem complex, but they offer unique benefits:

1. Deep Understanding of the EVM:

Working directly with Opcodes provides a deep understanding of how the EVM operates and offers insights that can be valuable for developers, researchers and educators.

2. Gas Optimization:

Opcodes allow developers to have the ability to optimize the code for gas efficiency — reducing transaction costs.

3. Customized Functionality:

Opcodes allow for a level of customization and control that may not be achievable through high-level languages. Developers can craft specific functionalities according to their unique requirements.

4. Security Analysis:

By examining the bytecode of a contract, auditors and security experts can identify vulnerabilities and ensure that the contract behaves as intended.

Step By Step Guide to Write a Smart Contract Using Opcodes

Writing smart contracts using opcodes can be overwhelming but let’s break down the task and learn how we can deploy our first smart contract using opcodes.

1. Understand the EVM Opcodes

Understanding opcodes is the first step. There are around 120 opcodes, each with different functionalities. Here are some examples:

To learn more about opcodes, you can check out this comprehensive guide. Practice using the EVM playground to get comfortable with opcodes.

2. Write the Opcode for the Smart Contract

Now once you’re comfortable with opcodes — outline the logic of your contract. Determine what functions you want to perform and how the data will be stored and manipulated.

For this guide, we are going to write a very simple opcode for summing two numbers.

Here’s how it looks like:

PUSH1 0X02
PUSH2 0X03
ADD

Let’s simplify these lines:

  1. PUSH1 0x02 pushes the value 0x02 onto the stack
  2. PUSH1 0x03 pushes the value 0x03 onto the stack
  3. ADD pops the top two values from the stack, adds them together, and pushes the result back onto the stack

3. Calculate Bytecode for the Opcode

Once we have the opcode for our contract. Lets understand how we can calculate the bytecode for the given opcode:

  1. PUSH1 0x02
  • PUSH1 The opcode for PUSH1 is 0x60
  • 0x02 The value to be pushed onto the stack
  • Combined, this gives the bytecode 0x6002
  1. PUSH1 0x03
  • PUSH1: The opcode for PUSH1 is 0x60.
  • 0x03: The value to be pushed onto the stack.
  • Combined, this gives the bytecode 0x6003.
  1. ADD
  • ADD: The opcode for ADD is 0x01.
  • There is no associated value, so the bytecode is simply 0x01.

Putting it all together, the complete bytecode for the given opcodes is:

0x6002600301

This bytecode can be used to deploy or interact with a contract on the Ethereum network.

4. Deploy the contract

Remix doesn’t provide a direct way to compile raw opcodes. So we’ll need to use a wrapper contract which will include the bytecode as a constructor. This will allow us to deploy the opcodes.

Here’s what our wrapper contract would look like:

pragma solidity ^0.8.0;
contract DeployOpcodes {
uint256 public sum;
constructor() {
deployOpcodes();
}
function deployOpcodes() private {
assembly {
let x := mload(0x40) // Find empty storage location
mstore(x, 0x02) // First number to add
mstore(add(x, 0x20), 0x03) // Second number to add
let result := add(mload(x), mload(add(x, 0x20))) // Sum the numbers
sstore(sum.slot, result) // Store the result in the "sum" state variable
}
}
function getSum() public view returns (uint256) {
return sum;
}
}

In this contract, we have several key components:

  1. Declaration of the Contract DeployOpcodes: This is the name of our contract, and it's where we'll house all the functions and variables related to our opcode deployment.
  2. Private Function deployOpcodes: Inside this function, we're going to work directly with the Ethereum Virtual Machine (EVM) using assembly language. Here's a breakdown of what each line does:

a. Assembly Block: We begin with the assembly keyword, allowing us to write low-level EVM code.

b. Finding an Empty Storage Location: The line let x := mload(0x40) loads the value at the memory location 0x40, a special location in the EVM that usually contains the “free memory pointer.” By assigning this value to variable x, we find an empty storage location.

c. Storing the First Number: Next, mstore(x, 0x02) stores the value 0x02 at the memory location pointed to by x. This represents the first number we want to add.

d. Storing the Second Number: The line mstore(add(x, 0x20), 0x03) stores the value 0x03 at the memory location x + 0x20. This represents the second number to add.

e. Calculating the Sum: With let result := add(mload(x), mload(add(x, 0x20))), we load the values at memory locations x and x + 0x20, add them together, and assign the result to a variable named result.

f. Storing the Result: Finally, sstore(sum.slot, result) stores the value of result in the storage slot corresponding to the sum state variable. This allows us to retrieve the sum later.

Now using Remix is easy! Compile the code and then hit the Deploy button!

You can see we have the getSum function which will return the value 5 to us.

Thats how we write our smart contracts using opcodes.

Writing smart contracts using opcodes is not just an academic exercise; it’s a powerful tool for developers who want to optimize, customize, and deeply understand their smart contracts.

If you found this guide helpful, please give it a clap and follow me for more insights!

Pari Tomar (Twitter || LinkedIn)

--

--

Pari Tomar

Researches, talks, shares know-how on building a career in blockchain space