Optimism + scaffold-eth 🏗 Dapp Starter Pack
Optimism’s Optimistic Rollups are approaching mainnet release! We spun up a dedicated branch of scaffold-eth in eager anticipation:
- Run a local chain (L1) with an Optimistic Rollup (L2)
- Interact with L1 and L2
- Move ETH between L1 and L2
- Deploy smart contracts on L2
- Create our own bridging ERC20 token!
Work-in-progress klaxon 🚨: this is a fresh build, on a brand-new protocol, so expect everything to evolve and change :). And feedback welcome!
If you want to get right to it: the code is here (step-by-step in the README).
Other folks have written in more detail about how Optimism’s Rollup works. This post is focused on what we found out getting up, running, and developing on Optimistic Ethereum…
Let’s go!
Running a local chain with a roll-up
You will need to have Docker installed!
Running several chains locally and having them talk to each other is not trivial. Thankfully the Optimism team have provided an out-of-the-box integration repository that runs the six Docker containers required. This is part of the local-optimism
branch, as a Git submodule. When you pull the repo down you will need to initiate & update the submodules, then it is a single command to stand the whole thing up.
cd docker/optimism-integration && make up
It feels a bit like a spaceship taking off!
Watching the logs gives you quite a practical sense of how Optimism works -first an L1 chain is initialised, and the core Optimism contracts are deployed, then a few services that relay information between L1 and L2 initialise, then finally the L2 geth implementation starts up.
Assuming everything is working nicely, we’re ready to go!
Interacting with the rollup & the local chain
One of the real benefits of the Optimism implementation is the compatibility with the EVM — in many ways it is as simple as changing your RPC URL & chain ID:
l1Local: { rpc: "http://localhost:9545", chainId: 31337 }l2Local: { rpc: "http://localhost:9545", chainId: 420 }l2Kovan: { rpc: "https://kovan.optimism.io", chainId: 69 }
There are of course some differences that need to be taken into account, and we will cover some as we go.
From a user and developer perspective, one of major things to consider is how to handle the L1 and L2 networks — which to surface to users, and how to ensure that connected wallets are connected to the right one.
The latter challenge is definitely a good use-case for the custom networks API (not yet implemented in this branch — PRs welcome!)
In this branch we instantiate two providers and two signers, as we want to support interaction with the local chain and the rollup.
Moving ETH between L1 and L2
Local rollups and the current deployment on Kovan don’t currently require any transaction fees, but this will be a key transition on mainnet. We have a simple OptimisticETHBridge
component, which shows a users balance on L1 and L2, and allows them to Deposit to L2 or Withdraw:
Depositing means calling the payable deposit
function on the L1ETHGateway
contract with the value you would like to deposit. This contract is deployed as part of the Optimism initialisation — the deploy address on the local setup is always the same (phew! check the deployment logs), but is different on Kovan.
On Optimism, there is no native ETH, ETH is just an ERC20 token(albeit one deployed at a predeployed address, which is the same on any rollups), and withdrawing is baked into the ERC20 contract:
await l2Tx(L2ETHGatewayContract.withdraw(
parseEther(values.amount.toString())))
This component also has simple “Send” functionality built in for L1 and L2.
Short-term gotchas that the Optimism team are working on:
- Sending a transaction with
{ value }
is not currently supported on L2, so we instantiate anethers.js
Contract and usetransfer
- The version of geth currently implemented on L2 does not throw on
transactionResponse
, as it does on L1 — it is necessary towait()
for atransactionReceipt
. In scaffold-eth this meant adding an additional line to our Transactor helper:
result = await signer.sendTransaction(tx);
await result.wait()
Deploying on Optimism: YourContract
One of Optimism’s primary focus areas has been on transferability, from the EVM to the OVM. As such only minor alterations were required to make our stock scaffold-eth contract viable on L2 — we just had to import the Optimism compiler in our hardhat configuration (which will then compile all contracts unless they have a //@unsupported: ovm
flag) and then use the Optimism ethers variant to deploy our contract.
const { l2ethers } = require("hardhat");...
contractArtifacts = await l2ethers.getContractFactory(contractName, signerProvider);
const deployed = await contractArtifacts.deploy(...contractArgs, overrides);
await deployed.deployTransaction.wait()
Note the aforementioned
wait()
!
There are some slight nuances — we were not able to use the inbuilt Hardhat network, and had to instantiate our own providers & signers.
We didn’t have to make any changes to our contract, though that might not necessarily be the case, for example calls to .balance
will throw an error when compiling. In general the compiler errors are helpful in tracking down any problems.
We did make some changes to dig into time on Optimism — block.timestamp
does exist, but is a reference to L1 time. Two things update the time on L2: Bridge messages from L1 to L2, and a “heartbeat” that regularly updates the L2 time on a set frequency.
This does create some interesting challenges in dealing with time on L2, as block.timestamp
will always be in the past. More thinking on this to come…
On a very practical note this means that in local development, you need regular transactions on your local chain to keep your L2 time up-to-date!
Bridging on Optimism: Old English ERC20
While for many use cases, the ETH bridge & token-bridges deployed by others will be all the L1<->L2 functionality required, we also wanted to understand what it took to move our own L1 ERC20 to L2 and back again.
Fortunately the Optimism team have some reference contracts as part of their contracts package, plus a helpful tutorial, so we were able to pull them into our branch. We will be deploying three contracts:
ERC20.sol
: on L1, this is the “source of truth” — a simple implementation with amint(value)
function that allows anyone mint themselves some tokens.L1ERC20Gateway.sol
: also on L1, this allows us to deposit to L2, locking the tokens while doing soL2DepositedERC20.sol
: this contract is deployed on L2, it is an ERC20 implementation that mints new tokens when they are deposited from L1, and burns them when they are withdrawn.
Deployment order is important, as the L1ERC20Gateway
needs to know about the ERC20
address and the L2DepositedERC20
address, then the L2DepositedERC20
contract needs to be activated via init()
with the L1ERC20Gateway
address, completing the connection. Our deployed contracts communicate with the L1Messenger
and L2Messenger
respectively to make deposits & withdrawals.
Once we have deployed, we can test the bridging functionality, either in the frontend app, or in the deployment script itself.
The L1ERC20Gateway
must be approved to move tokens to get the whole thing started.
There are conversations afoot to have a single “unibridge” for ERC20 tokens, so such individual bridges may not be necessary in production, but it remains a helpful proof-of-concept for local development.
What next?
Obviously the key next step is to go to a testnet (and then to mainnet!) This branch includes configuration options to go to the Kovan deployment of Optimism —it is as simple as updating the selectedNetwork
in App.js
, and the defaultNetwork
or --network
parameter when deploying from Hardhat.
But the bigger question is what to build on Optimism!
We’re going to be releasing more run-throughs, proof-of-concepts and maybe even full-fledged products over the coming weeks. Watch this space, and always happy to chat.
And if you haven’t already — pull down the branch and start building!
Many thanks to Ben & Kevin from Optimism for their helpful answers, and Austin Griffith for the help, hustle & support!