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.
Before you start
You need MetaMask with two networks added and the same account funded on both.
Fund the account on each side:
- Sepolia ETH from a public faucet, for example the Google Cloud Sepolia faucet.
- Testnet WBT from the Whitechain faucet, to pay gas on Whitechain Sepolia.
L1StandardBridge, the OptimismMintableERC20Factory (0x4200000000000000000000000000000000000012), the L2StandardBridge (0x4200000000000000000000000000000000000010), the OptimismPortal, and the DisputeGameFactoryProxy.
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 openmint method so you can refill later.
TestToken.sol
TestToken.sol
TestToken.sol
- Remix
- Hardhat
- Foundry
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.- Open the
OptimismMintableERC20Factoryat0x4200000000000000000000000000000000000012in the testnet explorer and go to the write-as-proxy tab. - Call
createOptimismMintableERC20WithDecimalswith your L1 token address as_remoteToken, a name and symbol for L2, and6for_decimals. - Open the transaction and read the emitted
OptimismMintableERC20Created(orStandardL2TokenCreated) event. The new L2 token address is in the log. - Verify the pairing on the new L2 token:
remoteToken()returns your L1 token, andbridge()returns theL2StandardBridge(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)
- On the L1 token in Sepolia Etherscan, call
approvewith theL1StandardBridgeas the spender and an amount at or above what you plan to deposit. - On the
L1StandardBridge, calldepositERC20through the write-as-proxy tab.
- After about 2 to 5 minutes, call
balanceOfon the L2 token with your address to confirm the balance arrived. - Optional: check the L1 token balance of the
L1StandardBridgeto 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 theL2StandardBridge (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
.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
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).
withdraw.ts
withdraw.ts
withdraw.ts
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.

