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
- Install Node.js (opens in a new tab), pnpm (opens in a new tab), and yarn (opens in a new tab).
- Access to L2 (OP Mainnet) endpoint. However, that L2 endpoint can be a read-only replica.
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
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
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
//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:
Variable | Meaning |
---|---|
L1URL | URL to L1 (Goerli if you followed the directions on this site) |
L2URL | URL to the L2 from which you are withdrawing |
PRIV_KEY | Private key for an account that has ETH on L2. It also needs ETH on L1 to submit transactions |
OPTIMISM_PORTAL_ADDR | Address 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
// 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);
});
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!");
});
});