OP Stack
Security
Forcing Withdrawal from Blockchain

Forcing Withdrawal from an OP Stack Blockchain

Any assets you own on an OP Stack blockchain are backed by equivalent assets on the underlying L1, locked in a bridge. In this tutorial, you learn how to withdraw these assets directly from L1.

Before You Begin

Setup

Make a Project Folder and Initialize It

mkdir project-folder-name
cd folder-name
pnpm init

Create Three Files for the Folder

  • Create a .env file
.env file
L1URL=<<< L1 URL goes here >>>
L2URL=<<< L2 URL goes here >>>
PRIV_KEY=<< private key goes here >>>
OPTIMISM_PORTAL_ADDR=<< address of OptimismPortalProxy >>
  • Create a hardhat.config.js file
hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require('dotenv').config();
 
// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();
 
  for (const account of accounts) {
    console.log(account.address);
  }
});
 
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more
 
/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.4",
  networks: {
    l1: {
         url: process.env.L1URL,
         accounts: [ process.env.PRIV_KEY ]
    }
  }
};
  • Create a Greeter.sol file
Greeter.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
 
import "hardhat/console.sol";
 
 
contract Greeter {
  string greeting;
 
  constructor(string memory _greeting) {
    console.log("Deploying a Greeter with greeting:", _greeting);
    greeting = _greeting;
  }
 
  function greet() public view returns (string memory) {
    return greeting;
  }
 
  function setGreeting(string memory _greeting) public {
    console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
    greeting = _greeting;
  }
}

Edit .env to set these variables:

VariableMeaning
L1URLURL to L1 (Goerli if you followed the directions on this site)
L2URLURL to the L2 from which you are withdrawing
PRIV_KEYPrivate key for an account that has ETH on L2. It also needs ETH on L1 to submit transactions
OPTIMISM_PORTAL_ADDRAddress of the OptimismPortalProxy on L1.

Install the dependencies

pnpm add @eth-optimism/sdk@^2.0.1 dotenv@^16.0.3 ethers@^5.7.2 ethereum-waffle@^3.4.4 @nomiclabs/hardhat-ethers@^2.2.2 @nomiclabs/hardhat-waffle@^2.0.5 hardhat@^2.13.0 chai@^4.3.7 @eth-optimism/contracts-bedrock@^0.13.1

Withdrawal

ETH withdrawals

The easiest way to withdraw ETH is to send it to the bridge, or the cross domain messenger, on L2.

Enter the Hardhat console

npx hardhat console --network l1

Specify the amount of ETH you want to transfer

This code transfers one hundred'th of an ETH.

transferAmt = BigInt(0.01 * 1e18)

Create a contract object for the OptimismPortal (opens in a new tab) contract

optimismContracts = require("@eth-optimism/contracts-bedrock")
optimismPortalData = optimismContracts.getContractDefinition("OptimismPortal")
optimismPortal = new ethers.Contract(process.env.OPTIMISM_PORTAL_ADDR, optimismPortalData.abi, await ethers.getSigner())

Send the transaction

txn = await optimismPortal.depositTransaction(
   optimismContracts.predeploys.L2StandardBridge,
   transferAmt,
   1e6, false, []
)
rcpt = await txn.wait()

To prove (opens in a new tab) and finalize (opens in a new tab) the message we need the hash

Optimism's core-utils package (opens in a new tab) has the necessary function.

optimismCoreUtils = require("@eth-optimism/core-utils")
withdrawalData = new optimismCoreUtils.DepositTx({
   from: (await ethers.getSigner()).address,
   to: optimismContracts.predeploys.L2StandardBridge,
   mint: 0,
   value: ethers.BigNumber.from(transferAmt),
   gas: 1e6,
   isSystemTransaction: false,
   data: "",
   domain: optimismCoreUtils.SourceHashDomain.UserDeposit,
   l1BlockHash: rcpt.blockHash,
   logIndex: rcpt.logs[0].logIndex,
})
withdrawalHash = withdrawalData.hash()

Create the object for the L1 contracts, as explained in the documentation

You will create an object similar to this one:

L1Contracts = {
   StateCommitmentChain: '0x0000000000000000000000000000000000000000',
   CanonicalTransactionChain: '0x0000000000000000000000000000000000000000',
   BondManager: '0x0000000000000000000000000000000000000000',
   AddressManager: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
   L1CrossDomainMessenger: '0x27E8cBC25C0Aa2C831a356bbCcc91f4e7c48EeeE',
   L1StandardBridge: '0x154EaA56f8cB658bcD5d4b9701e1483A414A14Df',
   OptimismPortal: '0x4AD19e14C1FD57986dae669BE4ee9C904431572C',
   L2OutputOracle: '0x65B41B7A2550140f57b603472686D743B4b940dB'
}

Create the data structure for the standard bridge

 bridges = { 
   Standard: { 
      l1Bridge: l1Contracts.L1StandardBridge, 
      l2Bridge: "0x4200000000000000000000000000000000000010", 
      Adapter: optimismSDK.StandardBridgeAdapter
   },
   ETH: {
      l1Bridge: l1Contracts.L1StandardBridge, 
      l2Bridge: "0x4200000000000000000000000000000000000010", 
      Adapter: optimismSDK.ETHBridgeAdapter
   }
}

Create a cross domain messenger (opens in a new tab)

This step, and subsequent ETH withdrawal steps, are explained in this tutorial.

optimismSDK = require("@eth-optimism/sdk")
l2Provider = new ethers.providers.JsonRpcProvider(process.env.L2URL)
await l2Provider._networkPromise
crossChainMessenger = new optimismSDK.CrossChainMessenger({
   l1ChainId: ethers.provider.network.chainId,
   l2ChainId: l2Provider.network.chainId,
   l1SignerOrProvider: await ethers.getSigner(),
   l2SignerOrProvider: l2Provider,
   bedrock: true,
   contracts: {
      l1: l1Contracts
   },
   bridges: bridges
})   

Wait for the message status for the withdrawal to become READY_TO_PROVE

By default the state root is written every four minutes, so you're likely to need to need to wait.

await crossChainMessenger.waitForMessageStatus(withdrawalHash, 
    optimismSDK.MessageStatus.READY_TO_PROVE)

Submit the withdrawal proof

await crossChainMessenger.proveMessage(withdrawalHash)

Wait for the message status for the withdrawal to become READY_FOR_RELAY

This waits the challenge period (7 days in production, but a lot less on test networks).

await crossChainMessenger.waitForMessageStatus(withdrawalHash, 
   optimismSDK.MessageStatus.READY_FOR_RELAY)

Finalize the withdrawal

See that your balance changes by the withdrawal amount.

myAddr = (await ethers.getSigner()).address
balance0 = await ethers.provider.getBalance(myAddr)
finalTxn = await crossChainMessenger.finalizeMessage(withdrawalHash)
finalRcpt = await finalTxn.wait()
balance1 = await ethers.provider.getBalance(myAddr)
withdrawnAmt = BigInt(balance1)-BigInt(balance0)

transferAmt > withdrawnAmt

Your L1 balance doesn't increase by the entire transferAmt because of the cost of crossChainMessenger.finalizeMessage, which submits a transaction.

Additional Files

sample-script.js
// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
// Runtime Environment's members available in the global scope.
const hre = require("hardhat");
 
async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');
 
  // We get the contract to deploy
  const Greeter = await hre.ethers.getContractFactory("Greeter");
  const greeter = await Greeter.deploy("Hello, Hardhat!");
 
  await greeter.deployed();
 
  console.log("Greeter deployed to:", greeter.address);
}
 
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
sample-test.js
const { expect } = require("chai");
 
describe("Greeter", function () {
  it("Should return the new greeting once it's changed", async function () {
    const Greeter = await ethers.getContractFactory("Greeter");
    const greeter = await Greeter.deploy("Hello, world!");
    await greeter.deployed();
 
    expect(await greeter.greet()).to.equal("Hello, world!");
 
    const setGreetingTx = await greeter.setGreeting("Hola, mundo!");
 
    // wait until the transaction is mined
    await setGreetingTx.wait();
 
    expect(await greeter.greet()).to.equal("Hola, mundo!");
  });
});