For an L1/L2 token pair to work on the Standard Bridge the L2 token contract must implement
IL2StandardERC20
interface.
If you do not need any special processing on L2, just the ability to deposit, transfer, and withdraw tokens, you can use OptimismMintableERC20Factory
.
Warning: The standard bridge does not support certain ERC-20 configurations:
-
Download the necessary packages.
yarn
-
Copy
.env.example
to.env
.cp .env.example .env
-
Edit
.env
to set the deployment parameters:MNEMONIC
, the mnemonic for an account that has enough ETH for the deployment.L1_ALCHEMY_KEY
, the key for the alchemy application for a Goerli endpoint.L2_ALCHEMY_KEY
, the key for the alchemy application for an OP Goerli endpoint.L1_TOKEN_ADDRESS
, the address of the L1 ERC20 which you want to bridge. The default value,0x32B3b2281717dA83463414af4E8CfB1970E56287
is a test ERC-20 contract on Goerli that lets you callfaucet
to give yourself test tokens.
-
Open the hardhat console.
yarn hardhat console --network optimism-goerli
-
Connect to
OptimismMintableERC20Factory
.fname = "node_modules/@eth-optimism/contracts-bedrock/artifacts/contracts/universal/OptimismMintableERC20Factory.sol/OptimismMintableERC20Factory.json" ftext = fs.readFileSync(fname).toString().replace(/\n/g, "") optimismMintableERC20FactoryData = JSON.parse(ftext) optimismMintableERC20Factory = new ethers.Contract( "0x4200000000000000000000000000000000000012", optimismMintableERC20FactoryData.abi, await ethers.getSigner())
-
Deploy the contract.
deployTx = await optimismMintableERC20Factory.createOptimismMintableERC20( process.env.L1_TOKEN_ADDRESS, "Token Name on L2", "L2-SYMBOL" ) deployRcpt = await deployTx.wait()
-
Get the token addresses.
l1Addr = process.env.L1_TOKEN_ADDRESS event = deployRcpt.events.filter(x => x.event == "OptimismMintableERC20Created")[0] l2Addr = event.args.localToken
-
Get the data for
OptimismMintableERC20
:fname = "node_modules/@eth-optimism/contracts-bedrock/artifacts/contracts/universal/OptimismMintableERC20.sol/OptimismMintableERC20.json" ftext = fs.readFileSync(fname).toString().replace(/\n/g, "") optimismMintableERC20Data = JSON.parse(ftext)
-
Get the L2 contract.
l2Contract = new ethers.Contract(l2Addr, optimismMintableERC20Data.abi, await ethers.getSigner())
-
Get the L1 wallet.
l1Url = `https://eth-goerli.g.alchemy.com/v2/${process.env.L1_ALCHEMY_KEY}` l1RpcProvider = new ethers.providers.JsonRpcProvider(l1Url) hdNode = ethers.utils.HDNode.fromMnemonic(process.env.MNEMONIC) privateKey = hdNode.derivePath(ethers.utils.defaultPath).privateKey l1Wallet = new ethers.Wallet(privateKey, l1RpcProvider)
-
Get the L1 contract.
l1Factory = await ethers.getContractFactory("OptimismUselessToken") l1Contract = new ethers.Contract(process.env.L1_TOKEN_ADDRESS, l1Factory.interface, l1Wallet)
-
Get tokens on L1 (and verify the balance)
faucetTx = await l1Contract.faucet() faucetRcpt = await faucetTx.wait() await l1Contract.balanceOf(l1Wallet.address)
Create and use CrossDomainMessenger
(the Optimism SDK object used to bridge assets).
The SDK supports multiple OP Chains: OP, Base, etc.
To see whether a specific OP Chain is supported directly, see the documentation.
Chains that aren't officially supported just take a few extra steps.
Get the L1 contract addresses, and provide them to the SDK.
Once you do that, you can use the SDK normally.
-
Import the Optimism SDK.
optimismSDK = require("@eth-optimism/sdk")
-
Create the cross domain messenger.
l1ChainId = (await l1RpcProvider.getNetwork()).chainId l2ChainId = (await ethers.provider.getNetwork()).chainId l2Wallet = await ethers.provider.getSigner() crossChainMessenger = new optimismSDK.CrossChainMessenger({ l1ChainId: l1ChainId, l2ChainId: l2ChainId, l1SignerOrProvider: l1Wallet, l2SignerOrProvider: l2Wallet })
-
Give the L1 bridge an allowance to use the user's token. The L2 address is necessary to know which bridge is responsible and needs the allowance.
depositTx1 = await crossChainMessenger.approveERC20(l1Contract.address, l2Addr, 1e9) await depositTx1.wait()
-
Check your balances on L1 and L2. Note that
l1Wallet
andl2Wallet
have the same address, so it doesn't matter which one we use.await l1Contract.balanceOf(l1Wallet.address) await l2Contract.balanceOf(l1Wallet.address)
-
Do the actual deposit
depositTx2 = await crossChainMessenger.depositERC20(l1Addr, l2Addr, 1e9) await depositTx2.wait()
-
Wait for the deposit to be relayed.
await crossChainMessenger.waitForMessageStatus(depositTx2.hash, optimismSDK.MessageStatus.RELAYED)
-
Check your balances on L1 and L2.
await l1Contract.balanceOf(l1Wallet.address) await l2Contract.balanceOf(l1Wallet.address)
-
Initiate the withdrawal on L2
withdrawalTx1 = await crossChainMessenger.withdrawERC20(l1Addr, l2Addr, 1e9) await withdrawalTx1.wait()
-
Wait until the root state is published on L1, and then prove the withdrawal. This is likely to take less than 240 seconds.
await crossChainMessenger.waitForMessageStatus(withdrawalTx1.hash, optimismSDK.MessageStatus.READY_TO_PROVE) withdrawalTx2 = await crossChainMessenger.proveMessage(withdrawalTx1.hash) await withdrawalTx2.wait()
-
Wait the fault challenge period (a short period on Goerli, seven days on the production network) and then finish the withdrawal.
await crossChainMessenger.waitForMessageStatus(withdrawalTx1.hash, optimismSDK.MessageStatus.READY_FOR_RELAY) withdrawalTx3 = await crossChainMessenger.finalizeMessage(withdrawalTx1.hash) await withdrawalTx3.wait()
-
Check your balances on L1 and L2. The balance on L2 should be back to zero.
await l1Contract.balanceOf(l1Wallet.address) await l2Contract.balanceOf(l1Wallet.address)