James Bachini

3 Examples Of How To Use Assembly In Solidity

Assembly Yul Solidity

Ethereum developers can directly use assembly in Solidity to improve the performance of their code. When OpenSea released the Seaport upgrade it reported the use of assembly reduced gas fees by 35% saving it’s users an estimated $460m per year. Today we are going to go through some simple examples of how and when to use assembly code in your solidity smart contracts.

  1. Why Use Assembly In Solidity
  2. Storing Data
  3. Sending ETH In Assembly
  4. Functions In Assembly
  5. Bits And Bytes
  6. Useful Opcodes
  7. Conclusion
James On YouTube

Why Use Assembly In Solidity

At any point when coding in solidity we can use the assembly { } keyword to start coding Yul which is a simplified, expanded version of assembly language. This provides lower level control over memory management and program control.

By using assembly we have direct access to the stack and can optimise our code to be more memory efficient saving the amount of gas required to execute transactions. This ultimately lowers the transaction costs for our users.

There is however a compromise in terms of readability. Many frontend devs will be able to read through a Solidity smart contract and understand the functions being executed and how to pull them into their web3 queries. Assembly in contrast can be a bit daunting and difficult to understand the logic and flow if you aren’t used to low level programming.

Assembly in Solidity
Return a contract name from assembly 🤮

The first time I saw this code I honestly thought it was the ugliest .sol file ever written. The abstraction of a name variable to unreadable hex code returned via assembly in a file which contains more comments than code still makes me cringe. But the use of assembly throughout that repository made the functions more gas efficient and will save their users a significant amount of funds along with providing a competitive advantage for OpenSea.

For me there’s a balance where assembly makes sense in simple low level functions and libraries which can be understood by the name of the function. I value code readability and being able to let a compiler handle most of the memory management when writing smart contracts so 99% of my code will be in Solidity rather than assembly. That being said understanding assembly and how it can improve efficiencies provides a useful tool which can be called on when writing a contract or afterwards during the unit testing and optimising stage to our workflow.


Storing Data

Let’s start with a simple example where we store data to storage and then recall it using two functions.

contract StoringData {
  function setData(uint256 newValue) public {
    assembly {
      sstore(0, newValue)
    }
  }

  function getData() public view returns(uint256) {
    assembly {
      let v := sload(0)
      mstore(0x80, v)
      return(0x80, 32)
    }
  }
}

The first function uses a single line of assembly and the sstore opcode to write the newValue variable to storage.

The second function uses sload to recall the data however this can’t be returned directly from storage so we first need to write it to memory with the mstore opcode. We then return the reference to where we stored that data in memory at the 0x80 location and the length of the data 32 bytes.


Sending ETH In Assembly

The next example is for a contract which holds ETH and has two owners. We want to introduce control logic in the form of for loops and if statements within assembly.

contract SendETH {
  address[2] owners = [0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,0xdD870fA1b7C4700F2BD7f44238821C26f7392148];
  function withdrawETH(address _to, uint256 _amount) external payable {
    bool success;
    assembly {
      for { let i := 0 } lt(i, 2) { i := add(i, 1) } {
        let owner := sload(i)
        if eq(_to, owner) {
          success := call(gas(), _to, _amount, 0, 0, 0, 0)
        } 
      }
    }
    require(success, "Failed to send ETH");
  }
}

When testing this we send the withdrawETH some ETH greater than or equal to the _amount value we pass through. We set an initial boolean called success to false and then jump into assembly.

The for loop uses the value i to loop through the code two times. We then use sload and the i value to get each of the owners. If the user defined _to value matches one of the owner accounts we use call opcode to send the funds.

Note that this code probably isn’t secure or a good use case for assembly because a lot of memory management and security checks are bypassed.


Functions In Assembly

Moving on to a more complex example we can start using functions to control the application flow and get more involved with editing the memory at a lower level.

contract UselessEncryption {
  function encrypt(string memory _input, bool _decrypt) external pure returns (string memory) {
    bytes32 output;
    assembly {
      function stringToBytes(a) -> b {
        b := mload(add(a, 32))
      }
      function addToBytes(bs,decrypt) -> r {
        if eq(decrypt, false) {
          mstore(0x0, add(bs,0x0101010101010101010101010101010101010101010101010101010101010101))
        } 
        if eq(decrypt, true) {
          mstore(0x0, sub(bs,0x0101010101010101010101010101010101010101010101010101010101010101))
        }
        r := mload(0x0)
      }
      let byteString := stringToBytes(_input)
      output := addToBytes(byteString,_decrypt)
    }
    bytes memory bytesArray = new bytes(32);
    for (uint256 i; i < 32; i++) bytesArray[i] = output[i];
    return string(bytesArray);
  }
}

This example of the worlds worse encryption simply takes a string of text and formats it into a bytes. It then checks to see if decrypt is set to true or false and either adds 01 to each bit or substracts it using the add and sub opcodes. We then convert it back to a string to return text from the function.

Note that the encrypted string will have 01’s padding any whitespace and it’ll only work with strings up to 32 characters. The contract is called UselessEncryption.sol for a reason but it provides a good example of using assembly functions to jump around and start working with lower level data.


Bits And Bytes

When using assembly you are exposed to the stack and low level data memory management. It’s important to have a good understanding of how this works.

All data is composed of 1’s and 0’s. These are known as bits, a uint256 variable for example will have 256 unique 1’s and 0’s to represent a fixed point number.

8 bits = 1 byte. So a byte will look something like this for the letter A 01000001. Ethereum’s virtual machine is built around 32 byte slots. 32*8= 256 which is why uint256 variables are used so widely in Solidity.

The more slots of storage and memory our contract requires the more expensive it will be in terms of gas costs. A practical example that can save gas is to keep require statement strings to below 32 characters to save taking up multiple slots i.e.

require(owner == _to, "Keep this short and sweet");

Data in the Ethereum virtual machine is processed on a stack which operates like an array with a last in, first out policy. New data to be stored is pushed to the top of the stack. Data to be removed from the stack is popped off the top.


Useful Opcodes

Here is a list of the Opcodes I think are most useful. There is a full list of opcodes here: https://docs.soliditylang.org/en/latest/yul.html

InstructionDescription
add(x,y)Adds the top two values in the stack together and replaces them with result
sub(x,y)Same for subtraction
mul(x,y)Same for multiplication
div(x,y)Same for division
mod(x,y)Same for modulus
lt(x,y)Less than. Returns 1 if x is less than y.
gt(x,y)Greater than
eq(x,y)Equal to. Returns 1 if x == y
not(x)Bitwise NOT (1010 > 0101)
and(x,y)Bitwise AND (1000 AND 1100 > 1000)
or(x,y)Bitwise OR (1000 AND 1100 > 1100)
xor(x,y)Bitwise XOR (1000 XOR 1100 > 0100)
byte(n,x)Nth byte of X
keccak256(pos,n)Native hashing algorithm
pop(x)Pop x off the stack
mload(pos)Load from memory at position pos
mstore(pos,value)Store value in memory at pos
sload(pos)Load from storage at position pos
sstore(pos,value)Store value in storage at pos
balance(address)Eth balance of an address in wei
call(gas,address,value,in,insize,out,ousize)Call an external contract, also used to send funds, returns 1 on success
delegatecall(gas,address,value,in,insize,out,ousize)As above but call from user instead of contract
revert(p,s)Revert transaction and any state changes
return(p,s)End execution returning data

Conclusion

Understanding how to use assembly in Solidity provides a tool for developers to gain more control over memory management and optimise gas costs. I think it’s likely we will see more and more Solidity repos using inline assembly and being able to read what is going on is a big benefit for developers.

Assembly is a lower level language compared to Solidity and understanding how memory is allocated to the stack, memory and storage will set you up with a good foundation to start experimenting.

I hope this introduction to assembly in solidity has been useful and you can jump into Remix or the Yul docs to take it from here.


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.