General Introduction

In this article, I would like to provide a post review and simple explanation of the layer-2 bridge bug, found by Jay Freeman (Saurik). There is an original blog post about his bug: https://www.saurik.com/optimism.html.

It was a long time ago, but when I read it for the first time (2 months ago) I knew just little about blockchain and all those words and techniques explained in the blog post seemed like magic to me. I am a tech guy and look for vulnerabilities on daily basis, I know that all those concepts are pretty simple and obvious it just requires a bit of domain knowledge and understanding of the terminology at least.

After 2 months of learning blockchain, I reread this article, and damn it is a super simple bug, the complexity is not too high. Yeah, it requires some understanding of the blockchain concepts, solidity, layer 2 solutions, but the general concept is easy! Let me explain it to you a bit easier and in my own way.

PS: What I really love in the Saurik blog post, is that he tells the story of the bug, and explains techniques of being stealthy on the network while testing a 2M$ vulnerability and more of the same. It is worth reading! This blog post turned me to start learning blockchain security. Big thank Saurik for the inspiration!

Introduction to Optimism

Optimism network is built on top of the Ethereum network and provides a faster experience for lower transaction cost, but to be compatible with Ethereum it needs to implement same set of instructions, or for better wording, I would say to have a complete alignment with the EVM specification.

** EVM - Ethereum virtual machine **

** OVM - Optimism virtual machine **

The bug in the Optimism layer 2 solution was found within the OVM SELFDESTRUCT opcode, opcodes are like specific instructions for the OVM, the SELFDESTRUCT opcode stands for deleting the deployed smart contract from the Optimism network and transferring all the remained funds to the address specified in the function argument. But the way it was implemented in OVM led to actual money printing. So let’s find out what was wrong.

Bug explanation

Any code execution of the smart contract starts with a transaction initiated by an external owned account (EOA). EOA is a normal user who has both private and public keys. Upon successful code execution, all the changes (the blockchain state updates) are added to the blockchain network.

The Saurik found that when the selfdestruct function is executed within the deployed smart contract, it transfers remained smart contract’s funds to the address specified in the function argument, but what was funny it does not update the blockchain state immediately and does not remove the remained funds until the user-initiated transaction is finished.

Let’s take a look at the malicious smart contract developed by Saurik and draw the chart to explain the things:

pragma solidity 0.7.6;

contract Exploit {
    constructor() payable {}

    function destroy() public {
        selfdestruct(payable(address(this)));
    }

    function take() public {
        msg.sender.transfer(address(this).balance);
    }
}

contract Attack {
    constructor(uint count) payable {
        Exploit exploit = new Exploit{value: msg.value}();
        for (; count != 0; --count)
            exploit.destroy();
        exploit.take();
        msg.sender.transfer(address(this).balance);
    }

    receive() external payable {}
}
  1. An attacker deploys Attack smart contract adding 2 ether to it. It is where the transaction starts.
  2. The Attack smart contract deploys Exploit smart contract moving 2 ether to it.
  3. The selfdestruct function is called in the loop iterating (let's say 10 times). At each selfdestruct execution, OVM adds remained funds to the address specified in the function argument. Each iteration will exponentially increase the contract’s “remaining” balance. 2^10 = 1024 ether.
  4. Since the transaction is not finished yet, the blockchain state is not updated, and “remaining” funds are not removed from the Exploit contract. Upon all the loop iterations, those “printed” funds are withdrawn to the Attack smart contract by calling the take function.