Skip to main content
This tutorial is testnet-only. It uses public faucets and the shortened testnet dispute delays. The timings here do not apply to mainnet, where a withdrawal takes minimum 7 days. See OP Stack canonical bridge for the mainnet figures.
This walkthrough runs a full canonical bridge deposit and withdrawal on Whitechain Sepolia and Ethereum Sepolia. It exists to verify the bridge hands-on: every step maps to a claim on the OP Stack canonical bridge page. Budget about 1 hour, up to about 3 hours in the worst case, most of it waiting for the withdrawal.

Before you start

You need MetaMask with two networks added and the same account funded on both. Fund the account on each side: The same address is used on both networks. The contract addresses this tutorial calls are listed on the canonical bridge page; the ones you need are the L1StandardBridge, the OptimismMintableERC20Factory (0x4200000000000000000000000000000000000012), the L2StandardBridge (0x4200000000000000000000000000000000000010), the OptimismPortal, and the DisputeGameFactoryProxy.
Use a dedicated testnet account, never a wallet that holds real funds. The withdrawal step loads a private key from a file. Never use a mainnet key, and never commit the key file.

1. Deploy a test ERC20 on Sepolia

You need a token pair to move. Deploy a minimal ERC20 on Sepolia. Use an OpenZeppelin ERC20 with 6 decimals, a constructor that mints a starting balance, and an open mint method so you can refill later.
TestToken.sol
Deploy it to Sepolia with whichever tool you prefer, then record the deployed L1 token address.
In Remix, deploy to the “Injected Provider” (MetaMask on Sepolia). Enable “Verify contract on Etherscan” in the deploy panel so the L1 token verifies automatically. Record the deployed L1 token address.

2. Create the L2 token pair

A deposit only works once the L2 side of the pair exists. Create it with the factory on Whitechain Sepolia.
  1. Open the OptimismMintableERC20Factory at 0x4200000000000000000000000000000000000012 in the testnet explorer and go to the write-as-proxy tab.
  2. Call createOptimismMintableERC20WithDecimals with your L1 token address as _remoteToken, a name and symbol for L2, and 6 for _decimals.
  3. Open the transaction and read the emitted OptimismMintableERC20Created (or StandardL2TokenCreated) event. The new L2 token address is in the log.
  4. Verify the pairing on the new L2 token: remoteToken() returns your L1 token, and bridge() returns the L2StandardBridge (0x4200000000000000000000000000000000000010).
Factory-created tokens show only raw bytecode in the explorer, so their read and write tabs are not available. To call the token from a UI, use the Remix “At Address” feature: paste the L2 token address against a minimal OptimismMintableERC20 interface (with balanceOf, remoteToken, and bridge) and interact from there.

3. Deposit (L1 to L2)

  1. On the L1 token in Sepolia Etherscan, call approve with the L1StandardBridge as the spender and an amount at or above what you plan to deposit.
  2. On the L1StandardBridge, call depositERC20 through the write-as-proxy tab.
  1. After about 2 to 5 minutes, call balanceOf on the L2 token with your address to confirm the balance arrived.
  2. Optional: check the L1 token balance of the L1StandardBridge to see the deposit held in escrow, which shows the lock-and-mint behavior.
A bridged token does not appear in MetaMask automatically. Import the L2 token address manually (Import tokens in MetaMask on Whitechain Sepolia) to see the balance in the wallet.

4. Withdraw (L2 to L1)

The withdrawal has an on-chain start you do by hand, then a prove and finalize sequence that cannot be done through the explorer. You run those two steps with a script.

Initiate the withdrawal on L2

On the L2StandardBridge (0x4200000000000000000000000000000000000010) in the testnet explorer, call bridgeERC20: Record the transaction hash. This is the L2 withdrawal transaction the script needs.

Prove and finalize with viem

Set up a small project and install viem.
Terminal
Create .env with a dedicated testnet key and the L2 withdrawal hash from the previous step. The Whitechain Sepolia RPC https://rpc.testnet.whitechain.io is archive-capable, which the proof generation (eth_getProof) requires.
.env
The script below uses viem OP Stack actions. Three details matter for Whitechain. First, the built-in whitechainSepolia chain does not carry the OP Stack contract addresses or a sourceId, so the script defines the L2 chain explicitly. Second, Whitechain runs fault proofs (dispute games), so the script uses getWithdrawalStatus to wait out the delays. The waitToFinalize action assumes the legacy l2OutputOracle model and does not apply here. Third, do not confuse the similarly named whitechainTestnet export in viem/chains, which is a different network (see the warning below).
viem/chains also exports a chain literally named whitechainTestnet, but that is a different network (chain id 2625, RPC rpc-testnet.whitechain.io). The network this guide uses, chain id 1874 with RPC rpc.testnet.whitechain.io, ships in viem as whitechainSepolia. Do not import whitechainTestnet here; use the explicit defineChain below instead.
withdraw.ts
Run it:
Terminal
--env-file loads the .env you created above. Without it process.env.PRIVATE_KEY is undefined and the script throws on the first line. The script leaves itself running through the wait. When it prints withdrawal finalized, confirm the L1 token balance on your address increased by the withdrawn amount, and that the L1StandardBridge escrow balance decreased by the same amount.

Timing expectations

Testnet timings observed during validation. Mainnet is far longer; see the canonical bridge timing parameters. The wait for a covering game can exceed one proposer interval, because the game must cover the exact L2 block of your withdrawal. If the latest game predates your transaction, you wait for the next one.

What you have verified

Completing this tutorial confirms the following claims on the canonical bridge page.