How the OP Stack Works: What Happens When You Mint a Base Day One NFT?

AfterDark Labs
8 min readAug 25, 2023

--

Base launched earlier this month, kicking off onchain summer. Part of the festivities involve minting new NFTs everyday. For participants, all you have to do is connect your wallet, click mint, sign the transaction, and an NFT arrives in your wallet. At least that’s what happens at a high-level. If you’re interested in gaining a deeper understanding of what the process looks like from start to finish, this article is for you.

Gathering Gas Fees

Ok, so we connected our Coinbase Wallet and clicked the big mint button. Now what? The first request the web page makes is to a Base RPC URL to get the chain’s current gas prices. The response from the HTTP request can be seen below:

Gas Fees for Base Layer 2

This is unsurprising, as we will need to know these gas prices so that we can form a proper transaction. Since the OP stack is EVM equivalent, its transactions take the same form as Ethereum transactions. A simplified example of a transaction object can be seen below:

Sample transaction object

Great, so we have our maxPriorityFeePerGas and the maxFeePerGas. Reviewing the above transaction object, we can spot a number of items in there that we already should have information for as well. We’ve already specified within our wallet the chainId, gasLimit, and our source address. It’s also reasonable to believe the front-end is aware of the destination address and any required amount. However, since seeing is believing, here is a snippet of the script on the web page that runs during minting. e in this case is the ERC721 drop contract that the front-end is already aware of:

Script containing contract address and function to be called

It would appear that all we need now is to gather the nonce, form our data and then craft a signature.

Yet, the next HTTP request made is not intended to gather any of these missing details. Instead, an eth_call request is made to a Base RPC node for a contract deployed at 0x420000000000000000000000000000000000000F:

This address is the OP (or in this case Base) Gas Price Oracle. To understand why our next call would be to a gas oracle, let’s take a step back and quickly review the design of rollups built using the OP stack.

Building Transactions

Rollups, such as Base, built on the OP stack have their own execution environment (op-geth) for modifying state on Layer 2. In addition to this, they also need to publish their transaction data to Ethereum Mainnet, after all this is what fundamentally makes them rollups. This means that transactions will have to pay gas for state changes on the Layer 2 level as well as gas for the publication of data to Layer 1. Let’s examine how the gas fee is calculated for data that will be published to Ethereum by answering a few probing questions:

  1. How does the OP stack transaction data actually get posted to Ethereum?

When a transaction is created in the OP stack, it is sent in two places. To a batcher and off for execution. The op-batcher compresses a batch of transactions and submits them to Ethereum Mainnet as calldata. The execution environment (op-geth) provides us with a new state after execution on Layer 2. A diagram of this process taken from the official OP docs can be seen below:

Transaction flow for OP stack rollups
  1. How does the op-batcher compress the transaction data?

Ok, so we know that Ethereum charges 4 gas per zero byte of calldata and 16 gas per non-zero byte of calldata. However, we also know that since Bedrock, the OP stack leverages compression to reduce the amount of calldata that needs to be posted to Ethereum mainnet. At a high-level, transaction data is batched together and zlib compressed into ~9.5mb (at the time of writing) channels. Since compression occurs on a batch level, each transaction should pay for a portion of its batch’s gas overhead.

  1. How does the op-batcher submit the transaction data?

After the op-batcher compresses the transaction data into a channel, it submits the channel in a single transaction as calldata to a contract on the Ethereum mainnet chain, called the Batch Inbox Address.

Adding all of this together gives us the following formula for the total price of a transaction on an OP stack rollup:

Total gas fee calculation for a transaction on Base

Where the fee scalar currently is ~0.68, a net decrease to the transaction’s pricing in Layer 1 gas terms, attributed to efficiency gains from data compression.

Ok, so now it becomes much clearer why we needed to gather the Layer 1 price of the mint transaction.

If we continue looking at the traffic, we see that the next thing that occurs is the app makes an RPC call to get the transaction count for our address:

This will be the nonce that needs to be included in our signed Base transaction for minting the NFT. In our case, this is a brand new wallet and the result is 0x0.

Now that our wallet has the gas required for the Layer 1 transaction and the nonce of our account, we just need the data for our transaction and then our literal signature of approval and we can submit our transaction for inclusion on the network!

But how do we get the data? Come to think of it, how did we already submit a transaction with data to the Layer 1 Gas Price Oracle earlier?

The data is supposed to contain information, among other things, regarding what contract we want to interact with and any function we want to call. In other words, the front-end needs the contract ABI to be able to properly form the data, request a signature of this data from the user’s wallet, and then send a request out to the designated RPC provider.

The reason the front-end already has the information required to form the data is because before we even clicked the mint button, several .js scripts were loaded on the website. One of these scripts contained the ABI for the ERC721 drop contract that will ultimately be used to mint us a Base day one NFT. Pictured below is a snippet of the ABI for the function that will be called when minting the Base day one NFT:

With the correct ABI, the front-end can create the transaction with all of the information we have already uncovered and then ask the user to sign the transaction through their wallet provider.

Sending Transactions

Now, we should have everything we need to send our mint transaction, but before that gets sent, Coinbase Wallet actually simulates the transaction using its own api:

This provides the user with a popup box giving them an idea of what the upcoming transaction will effectively do. In this case it showed that an NFT named Based would be sent from the zero address to our wallet. A snippet of the JSON output of the simulated transaction can be seen here:

Once we click confirm, our transaction is officially sent out to the RPC provider:

What does this do? Well, this sends a request to the RPC node to send out a pre-signed transaction on our behalf to the Base network. What happens from here? If you’d like a deeper dive for the actual broadcasting and execution of the transaction, there’s an excellent blog post that covers this in great detail here.

Settling Transactions on Layer 1

What is not covered in that blog is how the transactions are actually sent from Layer 2 to Layer 1. We’ll dig into this by referencing the op-batcher source code, after all the batcher is responsible for posting transactions to Layer 1.

To start, a new batch submitter service is created and started from within the op-batcher. Once this service has started, it executes the loop function. Within this function, the batcher loads blocks into memory which it ultimately gathers from the l2client.

In other words, the batcher is pulling down and reading from the Layer 2’s (Base’s) published block data.

This pulled down data is then formed into a TXData object which processes the blocks by adding them into the channel compression pipeline in batches. These batches are then RLP encoded and compressed on a per batch basis into an output channel. Finally, the entire channel is published to Layer 1 when the op-batcher sends it as calldata to the Batch Inbox Address.

If we look at the Batch Inbox Address, we can see that the channels are posted every minute on Base:

And that’s it! Very shortly after we’ve received confirmation of our transaction being included on Base, the transaction data will be posted to the Batch Inbox Address on Ethereum mainnet. Our NFT now lives on Ethereum as a compressed transaction and on Base as part of the client state.

If you’re interested in a smart contract audit or other security services, get the process started by visiting afterdarklabs.xyz or reaching out to info@afterdarklabs.xyz directly.

--

--

AfterDark Labs

https://afterdarklabs.xyz. Shining a light on the darkest corners of Web3. We offer collaborative and client-centric blockchain security solutions.