Withdraw legacy OP Mainnet tokens to Ethereum through the canonical bridge.
Static, client-side only. No backend, no custody, no contracts of its own, no fees, no liquidity. It calls the L2StandardBridge and the OptimismPortal through your wallet, exactly as Etherscan "write contract" would, with the resolver and safety checks done for you.
A CryptoOpsec app tool, styled to match dyor.cryptoopsec.com.
Tokens minted on OP Mainnet by the 2021-22 OVM_L2StandardTokenFactory (pre-Bedrock L2StandardERC20) expose l1Token() and l2Bridge(). After Bedrock the standard became IOptimismMintableERC20 with remoteToken() and bridge(). The bridge contracts still honour both interfaces, so the on-chain route works, but bridge UIs (Superbridge, Brid.gg) only look for remoteToken() and only list tokens from the Superchain token list. Holders of a legacy token see no way out. Reference case: ARROW, L2 0x78b3C724A2F663D11373C4a1978689271895256f to L1 0x736609D310B5F925531B5ad895925CB0586F6241.
- Resolve. Paste an L2 token address. Detects legacy or modern interface, checks the token reports the standard bridge, verifies the L1 counterpart has code and is a readable ERC-20 with matching decimals, reads the L1StandardBridge escrow balance (the most that can be paid out today). Anything else is refused with a typed reason.
- Withdraw. Amount validated against wallet balance and escrow. Recipient defaults to the connected wallet and requires an explicit "immutable after submission" confirmation. Submits
withdrawTo(l2Token, to, amount, 200000, 0x)on0x4200000000000000000000000000000000000010. - Track. Paste the L2 tx hash or scan the connected wallet for withdrawals. Status timeline (initiated, state root, proved, challenge window, finalizable, finalized) with a live countdown and a Prove or Finalize button that runs on Ethereum. Any funded wallet can pay for prove and finalize; tokens always go to the recipient set at initiation. Under fault proofs the finalizer must name the prover, which the app handles.
Fault-proof dispute games on OP Mainnet are permissionless: anyone can create one claiming any sequence number and any root. viem's stock getGame / getWithdrawalStatus / waitToProve pick any game numerically above the withdrawal, which makes a fresh withdrawal look provable against junk. This app selects games itself: a candidate must cover the withdrawal, must not claim a block or time beyond the current L2 head, and the output root it commits to is recomputed from L2 state (eth_getProof on the L2ToL1MessagePasser) and compared byte for byte before it is used for status or for building a proof. Proven withdrawals are classified from the portal's own checkWithdrawal plus the anchor state registry, so a proof against an invalidated game shows "prove again" rather than a stale status.
Since September 2026 OP Mainnet runs super-root games (game type 9). These sequence by L2 timestamp instead of block number, and their root claim is the hash of a preimage (version, timestamp, per-chain output roots) that the game stores as its extraData. The portal proves against the per-chain root, which is the ordinary output root of the L2 block whose timestamp equals the game's. The lib handles both families: it decodes the preimage, checks it hashes to the root claim, resolves the timestamp to the exact L2 block, verifies the root, and builds the proof from that block at the game's index. Only mainnet.optimism.io among the keyless L2 RPCs serves eth_getProof at historical blocks; it stays first in the fallback list, and a keyed archive RPC in VITE_L2_RPC is the robust choice for production.
- The page never holds funds and deploys nothing. Every write is a transaction your wallet shows you. Verify the target:
0x4200...0010(L2StandardBridge) on OP Mainnet, and the OptimismPortal proxy fromviem/chainson Ethereum. - Chain constants come from
viem/chainsandsrc/lib/resolver.ts; nothing is fetched from a token list or an API of ours. - Reads go to public RPCs (or your own via
VITE_L1_RPC/VITE_L2_RPC), with fallback rotation across two more public providers per chain. - Safety rails are deliberate: L1-code-exists, decimals match, escrow solvency, immutable-recipient confirm. The UI never strips them.
- Out of scope: fast exits, deposits, chains other than OP Mainnet, fee-on-transfer tokens, ETH withdrawals.
npm install
npm run dev # http://localhost:5173
npm run typecheck # tsc, strict
npm run test:unit # pure logic, no network
npm run build # dist/
Fork tier (ground truth against real chain state) needs foundry:
./scripts/forks.sh # anvil L1 fork :8545 and OP fork :9545 (L1_RPC / L2_RPC env to override)
npm run test:fork
npm run e2e:fork # scripted rehearsal: resolve, impersonate a holder, withdraw, discover, status
Manual wallet walkthrough on the forks: add the two anvil RPCs to MetaMask as custom networks (chain ids 1 and 10, so the app treats them as mainnets), impersonate a holder with anvil_impersonateAccount or fund a test key, then run the three screens.
Environment (optional): copy .env.example to .env.local.
src/lib/ framework-free core: resolver, withdrawals, amounts, chains, rpc, errors, retry, route, format, timeline, withdrawForm
src/ui/ React + wagmi + Tailwind: screens, components, hooks, wagmi config, viem clients
test/unit/ vitest, pure
test/fork/ vitest against the anvil forks; fixtures in test/fixtures.ts
scripts/ forks.sh, e2e-fork.ts, smoke-prod.ts, withdraw-finish.ts (CLI reference implementation)
deploy/ nginx site config and rsync deploy script for the CryptoOpsec VPS
Same box and pattern as DYOR: nginx serves dist/ as static files.
- DNS:
AorCNAMErecordbridge.cryptoopsec.comto the VPS. - On the VPS:
mkdir -p /var/www/bridge.cryptoopsec.com, installdeploy/nginx-bridge.conf,nginx -t && systemctl reload nginx,certbot --nginx -d bridge.cryptoopsec.com. - Locally: put keyed RPCs in
.env.productionif you have them, thendeploy/deploy.sh(build, rsync, smoke). npm run smoke:prodany time: resolves ARROW and USDC.e, rejects the OP token, discovers a known historical withdrawal, checks the site.
Cloudflare Pages works too: build command npm run build, output dist/, the same VITE_* variables.
MIT.