Token Withdrawal Examples
Withdrawal flow step-by-step - ERC20(On Rollup) -> ERC20/Native Coin on L1
1. Install Required Dependencies
The withdrawal scripts require several command-line tools. Install them before proceeding:
1.1 Install Foundry (cast)
cast is part of Foundry, an Ethereum toolkit maintained by Paradigm. It provides CLI utilities for interacting with smart contracts via JSON-RPC.
curl -L https://foundry.paradigm.xyz | bash
foundryupVerify installation:
cast --version1.2 Install jq (JSON processor)
Ubuntu/Debian:
sudo apt-get install jqmacOS:
brew install jqVerify:
jq --version1.3 Install bc (arbitrary precision calculator)
Ubuntu/Debian:
sudo apt-get install bcmacOS:
brew install bcVerify:
bc --version1.4 Install perl (usually pre-installed)
Ubuntu/Debian:
sudo apt-get install perlmacOS: Usually pre-installed, but if needed:
brew install perlVerify:
perl --version1.5 Install python3
Ubuntu/Debian:
sudo apt-get install python3macOS:
brew install python3Verify:
python3 --version2. Setup
Create a withdraw-config.env file with the following configuration. The scripts will automatically populate many fields as you progress through the steps.
Copy the following template to create your withdraw-config.env file:
# ============================================================================
# WITHDRAWAL CONFIGURATION FILE (withdraw-config.env)
# ============================================================================
# Fill in the parameters below with your specific values.
# Parameters marked as "AUTO-FILLED" will be populated by the scripts.
# ============================================================================
# ----------------------------------------------------------------------------
# WALLET CONFIGURATION
# ----------------------------------------------------------------------------
# Your private key (DO NOT SHARE THIS!)
PRIVATE_KEY="<your-private-key>"
# Your wallet address (derived from private key)
# e.g., cast wallet address --private-key $PRIVATE_KEY
WALLET="<your-wallet-address>"
# ----------------------------------------------------------------------------
# NETWORK CONFIGURATION
# ----------------------------------------------------------------------------
# Rollup RPC endpoint URL
# e.g., https://fusion-rollup.quant.dev/rpc/<your-client-id>
ROLLUP_RPC_URL="<your-rollup-RPC-url>"
# L1 RPC endpoint URL
# e.g., http://localhost:8545 or https://mainnet.infura.io/v3/YOUR-PROJECT-ID
L1_RPC_URL="<your-L1-RPC-url>"
# Rollup chain ID (e.g., 73073 for custom rollup)
ROLLUP_CHAIN_ID="<rollup-chain-id>"
# L1 chain ID (e.g., 1337 for local devnet, 1 for Ethereum mainnet)
L1_CHAIN_IDS="<L1-chain-id>"
# ----------------------------------------------------------------------------
# ROLLUP CONTRACT ADDRESSES (Predeploys - same for all Optimism-based rollups)
# ----------------------------------------------------------------------------
# Rollup Standard Bridge predeploy address
ROLLUP_STANDARD_BRIDGE_ADDRESS="0x4200000000000000000000000000000000000010"
# Rollup Token Whitelist predeploy address
ROLLUP_WHITELIST_ADDRESS="0x4200000000000000000000000000000000000029"
# ----------------------------------------------------------------------------
# L1 CONTRACT ADDRESSES (Deployment-specific - get from your L1 deployment)
# ----------------------------------------------------------------------------
# L1 Standard Bridge proxy address
L1_STANDARD_BRIDGE_PROXY_ADDRESS="<L1-standard-bridge-proxy>"
# Delayed WETH proxy address
DELAYED_WETH_PROXY_ADDRESS="<delayed-weth-proxy>"
# Dispute Game Factory proxy address
DISPUTE_GAME_FACTORY_PROXY_ADDRESS="<dispute-game-factory-proxy>"
# Optimism Portal proxy address
OPTIMISM_PORTAL_PROXY_ADDRESS="<optimism-portal-proxy>"
# Anchor State Registry proxy address
ANCHOR_STATE_REGISTRY_PROXY_ADDRESS="<anchor-state-registry-proxy>"
# ETH Lockbox address (optional, for balance checks)
L1_ETH_LOCK_BOX_PROXY_ADDRESS="<eth-lockbox-address>"
# ----------------------------------------------------------------------------
# TOKEN ADDRESSES
# ----------------------------------------------------------------------------
# L1 ERC20 token address (the token on L1 that maps to your Rollup token)
# IMPORTANT: If the token is ERC20 on Rollup and NATIVE on L1, set this to:
# L1_TOKEN="0x0000000000000000000000000000000000000000"
L1_TOKEN="<L1-token-address>"
# Rollup ERC20 token address (the token you're withdrawing from Rollup)
# This should always be the actual Rollup token contract address
ROLLUP_TOKEN="<rollup-token-address>"
# L1 QNT Wrapper token address (if using wrapped QNT)
L1_WRAPPER_TOKEN="<L1-wrapper-token-address>"
# Token decimals (e.g., 18 for most ERC20, 6 for USDC)
TOKEN_DECIMALS="18"
# ----------------------------------------------------------------------------
# WITHDRAWAL PARAMETERS
# ----------------------------------------------------------------------------
# Amount to withdraw (in base units, e.g., wei for 18 decimals)
# Example: "1000000000000000000" = 1 token with 18 decimals
WITHDRAW_AMOUNT="1000000"
# Recipient address (leave empty to withdraw to your own wallet)
WITHDRAW_TO=""
# Set to "true" for ETH withdrawals, "false" for ERC20
WITHDRAW_ETH="false"
# Minimum gas limit for the withdrawal message (default: 200000)
MIN_GAS_LIMIT="200000"
# Extra data to attach to withdrawal (default: 0x)
EXTRA_DATA="0x"
# Source chain ID (leave empty to use ROLLUP_CHAIN_ID)
SOURCE_CHAIN_ID=""
# Destination chain ID (leave empty to use L1_CHAIN_IDS)
DESTINATION_CHAIN_ID=""
# ============================================================================
# AUTO-FILLED PARAMETERS - Leave these empty, scripts will populate them
# ============================================================================
# --- Filled by Step 3: Withdraw tokens from Rollup to L1 ---
ROLLUP_WITHDRAW_TX_HASH="" # [Step 3] Transaction hash of your Rollup withdrawal
ROLLUP_BLOCK_NUMBER="" # [Step 3] Rollup block number of withdrawal transaction
# --- Filled by Step 4: Extract withdrawal parameters from Rollup transaction ---
WITHDRAWAL_NONCE="" # [Step 4] Withdrawal nonce from RollupToL1MessagePasser
WITHDRAWAL_SENDER="" # [Step 4] Sender address (Rollup CrossDomainMessenger)
WITHDRAWAL_TARGET="" # [Step 4] Target address on L1 (L1 CrossDomainMessenger)
WITHDRAWAL_VALUE="" # [Step 4] Value in wei sent with withdrawal (0 for ERC20)
WITHDRAWAL_GAS_LIMIT="" # [Step 4] Gas limit for withdrawal execution on L1
WITHDRAWAL_DATA="" # [Step 4] Encoded withdrawal data (contains token transfer info)
WITHDRAWAL_DESTINATION_CHAIN_ID="" # [Step 4] Destination chain ID (extracted from MessagePassed event)
WITHDRAWAL_IS_NATIVE_TO_ERC20="" # [Step 4] Flag indicating if native-to-ERC20 withdrawal
WITHDRAWAL_HASH="" # [Step 4] Keccak256 hash of the withdrawal transaction
PROOF_SUBMITTER="" # [Step 4] Address that submitted the proof (defaults to WALLET)
WITHDRAWAL_BLOCK_NUMBER="" # [Step 4] Block number where withdrawal occurred
# --- Filled by Step 5: Find dispute game for withdrawal ---
DISPUTE_GAME_INDEX="" # [Step 5] Index of dispute game that includes your withdrawal
# --- Filled by Step 6: Get prove parameters (output root proof) ---
OUTPUT_ROOT_PROOF_VERSION="" # [Step 6] Output root proof version (always 0 for Bedrock)
OUTPUT_ROOT_PROOF_STATE_ROOT="" # [Step 6] Rollup state root at game's Rollup block
OUTPUT_ROOT_PROOF_ROLLUP_BLOCK_HASH="" # [Step 6] Rollup block hash at game's Rollup block
OUTPUT_ROOT_PROOF_MESSAGE_PASSER_STORAGE_ROOT="" # [Step 6] Storage root of RollupToL1MessagePasser at game's Rollup block
GAME_ROLLUP_BLOCK_NUMBER="" # [Step 6] Rollup block number that dispute game covers
# --- Filled by Step 7: Generate withdrawal Merkle proof ---
WITHDRAWAL_PROOF="" # [Step 7] Merkle proof that withdrawal exists in Rollup state
WITHDRAWAL_STORAGE_SLOT="" # [Step 7] Storage slot where withdrawal is recorded in RollupToL1MessagePasserNow you will need to fill in the withdrawal parameters for your env file. This withdrawal method requires a few different contracts that are detailed below. You need to choose the ones relevant to the blockchain you are withdrawing to:
| Smart Contract | Blockchain | Address |
|---|---|---|
| L1 Standard Bridge Proxy | Ethereum Sepolia testnet | 0xca7b5227a6983a1d5c5841df2e8edff12bf59deb |
| L1 Standard Bridge Proxy | Polygon Amoy testnet | 0x4603d005f60d4f6a4ed54279274767d5f47d3b63 |
| L1 Standard Bridge Proxy | Quant Besu Private Network | 0xe32ca85bee804c26ece4106c6bde609cc92a5e94 |
| Delayed WETH Proxy | Ethereum Sepolia testnet | 0x9c98ac07c230b226d99a2833f0ab32e7ffa31174 |
| Delayed WETH Proxy | Polygon Amoy testnet | 0x60ac2b841f2cfa9d1ea8359c1448873ed10db30d |
| Delayed WETH Proxy | Quant Besu Private Network | 0x4603316187785caede30c8b2cfa0f4adba8c7bee |
| Dispute Game Factory Proxy | Ethereum Sepolia testnet | 0x3dd3c48b5cd4dd19c2fd9ccb109e51b31dc633b7 |
| Dispute Game Factory Proxy | Polygon Amoy testnet | 0xdfb41346ca3f977890c42c92d782455498fc46fe |
| Dispute Game Factory Proxy | Quant Besu Private Network | 0xb6247b5f8ddcf23113d057aafa2b53ccd6d3c497 |
| Optimism Portal Proxy | Ethereum Sepolia testnet | 0x57416c27ffb863957b05da9ce8ecd9db6e3a9682 |
| Optimism Portal Proxy | Polygon Amoy testnet | 0x9a953d672f813cbf99fafc6d928dc9c0224f9b84 |
| Optimism Portal Proxy | Quant Besu Private Network | 0x0ac541fa9273e9a31a789f94112623277c6e83b0 |
| Anchor State Registry Proxy | Ethereum Sepolia testnet | 0xf1e5c62a2462dd68ae324153b0aced6ba1248555 |
| Anchor State Registry Proxy | Polygon Amoy testnet | 0x16e5bec945f013b66d530bd1974748207ea88687 |
| Anchor State Registry Proxy | Quant Besu Private Network | 0x338c8460564a7fe56e7b6a3f1540d47707f70513 |
| ETH Lockbox | Ethereum Sepolia testnet | 0x47f9ecb269f47d0410ec4f3e227f4d191b560b2e |
| ETH Lockbox | Polygon Amoy testnet | 0xa3f13e6f7b090aa37411e02d399d9ed18612b9a5 |
| ETH Lockbox | Quant Besu Private Network | 0xbbbb8a48532f726ff6a077b5926f3c3156117cca |
Next, copy the following script to you machine.
Warning: This is demo script to prove functionality, it is not recommended to use this in live production.
#!/bin/bash
# Withdraw Finalization Script (L1 side)
# This script helps you:
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
CONTRACTS_BEDROCK_DIR="$SCRIPT_DIR"
# Default config files (try multiple locations)
if [ -f "$SCRIPT_DIR/withdraw-config.env" ]; then
CONFIG_FILE="$SCRIPT_DIR/withdraw-config.env"
else
CONFIG_FILE="withdraw-config.env"
fi
# Parse named parameters
ACTION=""
while [[ $# -gt 0 ]]; do
case $1 in
--config)
CONFIG_FILE="$2"
shift 2
;;
*)
# First non-option argument becomes the action
if [[ -z "$ACTION" ]]; then
ACTION="$1"
fi
shift
;;
esac
done
# Configuration - source the config file
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: Config file $CONFIG_FILE not found"
echo "Please create withdraw-config.env or specify with --config"
exit 1
fi
source "$CONFIG_FILE"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
log_step() {
echo -e "${GREEN}[STEP]${NC} $1"
}
log_verify() {
echo -e "${YELLOW}[VERIFY]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
# Change to contracts-bedrock directory for cast commands
cd "$CONTRACTS_BEDROCK_DIR" || {
echo "Error: Could not change to contracts-bedrock directory"
exit 1
}
TOKENS_FILE="env.tokens"
if [ -f "$TOKENS_FILE" ]; then
# Only load tokens that aren't already set in config
if [ -z "${L1_TOKEN:-}" ] || [ -z "${L2_TOKEN:-}" ]; then
# Save current values before sourcing
SAVED_L1_TOKEN="${L1_TOKEN:-}"
SAVED_L2_TOKEN="${L2_TOKEN:-}"
source "$TOKENS_FILE"
# Restore saved values if they existed, otherwise use loaded values
if [ -n "$SAVED_L1_TOKEN" ]; then
L1_TOKEN="$SAVED_L1_TOKEN"
elif [ -n "${L1_TOKEN:-}" ]; then
log_info "Loaded L1_TOKEN: $L1_TOKEN from $TOKENS_FILE"
fi
if [ -n "$SAVED_L2_TOKEN" ]; then
L2_TOKEN="$SAVED_L2_TOKEN"
elif [ -n "${L2_TOKEN:-}" ]; then
log_info "Loaded L2_TOKEN: $L2_TOKEN from $TOKENS_FILE"
fi
fi
fi
# Set defaults for withdrawal
SOURCE_CHAIN_ID="${SOURCE_CHAIN_ID:-$L2_CHAIN_ID}"
DESTINATION_CHAIN_ID="${DESTINATION_CHAIN_ID:-$L1_CHAIN_IDS}"
WITHDRAW_TO="${WITHDRAW_TO:-$WALLET}"
require_withdrawal_params() {
local missing=0
for var in WITHDRAWAL_NONCE WITHDRAWAL_SENDER WITHDRAWAL_TARGET WITHDRAWAL_VALUE WITHDRAWAL_GAS_LIMIT WITHDRAWAL_DATA; do
if [ -z "${!var:-}" ]; then
log_error "$var is not set. Please set it in withdraw-config.env"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
exit 1
fi
}
# Verification functions from withdraw-verify.sh
# Verify 1: L2 wallet ETH balance
verify_1() {
log_verify "Verifying L2 wallet ETH balance"
BALANCE=$(cast balance $WALLET --rpc-url $L2_RPC_URL 2>/dev/null)
# Convert to human-readable format (wei to ETH)
if [ -n "$BALANCE" ] && [ "$BALANCE" != "0" ] && [ "$BALANCE" != "null" ]; then
BALANCE_DEC=$(cast --to-dec "$BALANCE" 2>/dev/null || echo "$BALANCE")
# Remove scientific notation if present
BALANCE_DEC=$(echo "$BALANCE_DEC" | sed 's/\[.*\]//' | tr -d ' ')
# Convert wei to ETH: balance / 10^18
BALANCE_ETH=$(echo "scale=18; $BALANCE_DEC / (10^18)" | bc 2>/dev/null || echo "N/A")
echo "ETH Balance for address $WALLET on L2: $BALANCE_DEC wei = $BALANCE_ETH ETH"
else
echo "ETH Balance for address $WALLET on L2: $BALANCE wei"
fi
if [ "$BALANCE" != "0" ] && [ -n "$BALANCE" ] && [ "$BALANCE" != "null" ]; then
echo -e "${GREEN}✓ Wallet has ETH balance on L2${NC}"
else
echo -e "${YELLOW}⚠ Wallet has no ETH balance on L2${NC}"
fi
}
# Verify 2: L2 token balance (if ERC20 withdrawal)
verify_2() {
if [ "$WITHDRAW_ETH" = "true" ] || [ -z "$L2_TOKEN" ]; then
log_info "Skipping L2 token balance check (ETH withdrawal or L2_TOKEN not set)"
return 0
fi
log_verify "Verifying L2 token balance"
BALANCE=$(cast call $L2_TOKEN "balanceOf(address)(uint256)" $WALLET --rpc-url $L2_RPC_URL 2>/dev/null)
# Get token decimals (from env or contract)
if [ -n "${TOKEN_DECIMALS:-}" ]; then
DECIMALS="$TOKEN_DECIMALS"
log_info "Using TOKEN_DECIMALS from config: $DECIMALS"
else
DECIMALS=$(cast call $L2_TOKEN "decimals()(uint8)" --rpc-url $L2_RPC_URL 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
if [ -n "$DECIMALS" ] && [ "$DECIMALS" != "null" ]; then
DECIMALS=$(cast --to-dec "$DECIMALS" 2>/dev/null || echo "$DECIMALS")
log_info "Fetched decimals from contract: $DECIMALS"
else
DECIMALS="18" # Default to 18 if not found
log_info "Using default decimals: $DECIMALS"
fi
fi
# Convert balance to decimal and human-readable
if [ -n "$BALANCE" ] && [ "$BALANCE" != "0" ] && [ "$BALANCE" != "null" ]; then
BALANCE_DEC=$(cast --to-dec "$BALANCE" 2>/dev/null || echo "$BALANCE")
# Remove scientific notation if present
BALANCE_DEC=$(echo "$BALANCE_DEC" | sed 's/\[.*\]//' | tr -d ' ')
# Convert to human-readable: balance / 10^decimals
BALANCE_HUMAN=$(echo "scale=$DECIMALS; $BALANCE_DEC / (10^$DECIMALS)" | bc 2>/dev/null || echo "N/A")
echo "L2 Token Balance for address $WALLET: $BALANCE_DEC (raw) = $BALANCE_HUMAN (with $DECIMALS decimals)"
else
echo "L2 Token Balance for address $WALLET: $BALANCE"
fi
if [ "$BALANCE" != "0" ] && [ -n "$BALANCE" ] && [ "$BALANCE" != "null" ]; then
echo -e "${GREEN}✓ Wallet has L2 token balance${NC}"
# Check if balance is sufficient for withdrawal
if [ -n "$WITHDRAW_AMOUNT" ]; then
# WITHDRAW_AMOUNT is already in base units (smallest unit), convert to human-readable
WITHDRAW_AMOUNT_DEC=$(echo "$WITHDRAW_AMOUNT" | sed 's/\[.*\]//' | tr -d ' ')
WITHDRAW_AMOUNT_HUMAN=$(echo "scale=$DECIMALS; $WITHDRAW_AMOUNT_DEC / (10^$DECIMALS)" | bc 2>/dev/null || echo "N/A")
BALANCE_DEC=$(cast --to-dec "$BALANCE" 2>/dev/null || echo "$BALANCE")
# Remove scientific notation if present
BALANCE_DEC=$(echo "$BALANCE_DEC" | sed 's/\[.*\]//' | tr -d ' ')
# Format numbers with commas for readability (using sed)
WITHDRAW_AMOUNT_PRETTY=$(echo "$WITHDRAW_AMOUNT_DEC" | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta')
BALANCE_PRETTY=$(echo "$BALANCE_DEC" | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta')
echo " Withdrawal amount: $WITHDRAW_AMOUNT_PRETTY (base units) = $WITHDRAW_AMOUNT_HUMAN tokens (with $DECIMALS decimals)"
echo " Current balance: $BALANCE_PRETTY (base units) = $BALANCE_HUMAN tokens"
if [ "$(echo "$BALANCE_DEC >= $WITHDRAW_AMOUNT_DEC" | bc 2>/dev/null)" = "1" ] || [ "$BALANCE_DEC" -ge "$WITHDRAW_AMOUNT_DEC" ] 2>/dev/null; then
echo -e "${GREEN}✓ Balance is sufficient for withdrawal amount ($WITHDRAW_AMOUNT_HUMAN tokens with $DECIMALS decimals)${NC}"
else
echo -e "${RED}✗ Balance ($BALANCE_PRETTY = $BALANCE_HUMAN tokens) is insufficient for withdrawal amount ($WITHDRAW_AMOUNT_PRETTY = $WITHDRAW_AMOUNT_HUMAN tokens with $DECIMALS decimals)${NC}"
fi
fi
else
echo -e "${YELLOW}⚠ Wallet has no L2 token balance${NC}"
fi
}
# Step 0: Set QNT Token L1 on L2 TokenWhitelist
step_0_set_qnt_token_l2() {
log_step "Setting QNT token address on L2 TokenWhitelist"
if [ -z "${L1_WRAPPER_TOKEN:-}" ]; then
log_error "L1_WRAPPER_TOKEN not set. Please set it in withdraw-config.env"
return 1
fi
if [ -z "$L2_WHITELIST_ADDRESS" ]; then
log_error "L2_WHITELIST_ADDRESS not set in config"
return 1
fi
# Set on L2 TokenWhitelist
log_info "Setting QNT_TOKEN_L1 on L2 TokenWhitelist..."
forge script scripts/TokenWhitelistInteraction.s.sol:TokenWhitelistInteraction \
--rpc-url "$L2_RPC_URL" \
--private-key "$PRIVATE_KEY" \
--sig 'setQNTTokenL1(address,address)' \
"$L2_WHITELIST_ADDRESS" \
"$L1_WRAPPER_TOKEN" \
--broadcast
# Verify the QNT token address was set correctly on L2
log_verify "Verifying QNT_TOKEN_L1 setting on L2"
QNT_TOKEN_L2=$(cast call $L2_WHITELIST_ADDRESS "QNT_TOKEN_L1()(address)" --rpc-url $L2_RPC_URL)
echo "L2 QNT_TOKEN_L1 is set to: $QNT_TOKEN_L2"
# Validate the value matches what we expect on L2
if [ "$QNT_TOKEN_L2" != "$L1_WRAPPER_TOKEN" ]; then
log_error "L2 QNT_TOKEN_L1 mismatch! Expected: $L1_WRAPPER_TOKEN, Got: $QNT_TOKEN_L2"
return 1
fi
echo -e "${GREEN}L2 QNT token address set successfully!${NC}"
}
# Step 1: Check L2 balance (using verification functions)
step_1_check_l2_balance() {
log_step "Checking L2 balance"
verify_1
echo ""
verify_2
}
# Step 2: Approve L2 Standard Bridge to spend tokens (only for ERC20)
step_2_approve_l2_bridge() {
log_step "Approving L2 Standard Bridge to spend tokens"
if [ "$WITHDRAW_ETH" = "true" ] || [ -z "$L2_TOKEN" ]; then
log_info "Skipping approval step (ETH withdrawal or L2_TOKEN not set)"
return 0
fi
if [ -z "$L2_TOKEN" ]; then
log_error "L2_TOKEN not set. Source .env.tokens or set in config"
return 1
fi
if [ -z "$WITHDRAW_AMOUNT" ]; then
log_error "WITHDRAW_AMOUNT not set in config"
return 1
fi
log_info "Approving $WITHDRAW_AMOUNT of $L2_TOKEN for withdrawal"
cast send $L2_TOKEN \
"approve(address,uint256)" \
$L2_STANDARD_BRIDGE_ADDRESS \
$WITHDRAW_AMOUNT \
--rpc-url $L2_RPC_URL \
--private-key $PRIVATE_KEY
log_verify "Verifying allowance"
cast call $L2_TOKEN \
"allowance(address,address)(uint256)" \
$WALLET \
$L2_STANDARD_BRIDGE_ADDRESS \
--rpc-url $L2_RPC_URL
}
# Step 3: Withdraw tokens from L2 to L1
step_3_withdraw() {
log_step "Withdrawing tokens from L2 to L1"
if [ -z "$L2_TOKEN" ] && [ -z "$WITHDRAW_ETH" ]; then
log_error "L2_TOKEN not set and WITHDRAW_ETH not set. Set one of them in config"
return 1
fi
if [ -z "$WITHDRAW_AMOUNT" ] && [ -z "$WITHDRAW_ETH" ]; then
log_error "WITHDRAW_AMOUNT not set in config"
return 1
fi
# Set default values if not provided
WITHDRAW_TO="${WITHDRAW_TO:-$WALLET}"
MIN_GAS_LIMIT="${MIN_GAS_LIMIT:-200000}"
EXTRA_DATA="${EXTRA_DATA:-0x}"
SOURCE_CHAIN_ID="${SOURCE_CHAIN_ID:-$L2_CHAIN_ID}"
DESTINATION_CHAIN_ID="${DESTINATION_CHAIN_ID:-$L1_CHAIN_IDS}"
if [ "$WITHDRAW_ETH" = "true" ] || [ -z "$L2_TOKEN" ]; then
# Withdraw ETH
log_info "Withdrawing ETH: $WITHDRAW_AMOUNT"
log_info "From: $WALLET"
log_info "To: $WITHDRAW_TO"
log_info "Source Chain ID: $SOURCE_CHAIN_ID"
log_info "Destination Chain ID: $DESTINATION_CHAIN_ID"
# For ETH withdrawal, use address(0) as token and send value
if [ "$WITHDRAW_TO" != "$WALLET" ] && [ -n "$WITHDRAW_TO" ]; then
TXHASH_OUTPUT=$(cast send $L2_STANDARD_BRIDGE_ADDRESS \
"withdrawTo(address,address,uint256,uint32,bytes,uint256,uint256)" \
"0x0000000000000000000000000000000000000000" \
$WITHDRAW_TO \
$WITHDRAW_AMOUNT \
$MIN_GAS_LIMIT \
$EXTRA_DATA \
$SOURCE_CHAIN_ID \
$DESTINATION_CHAIN_ID \
--rpc-url $L2_RPC_URL \
--private-key $PRIVATE_KEY \
--value $WITHDRAW_AMOUNT 2>&1)
OUTPUT="$TXHASH_OUTPUT"
echo "$OUTPUT"
else
TXHASH_OUTPUT=$(cast send $L2_STANDARD_BRIDGE_ADDRESS \
"withdraw(address,uint256,uint32,bytes,uint256,uint256)" \
"0x0000000000000000000000000000000000000000" \
$WITHDRAW_AMOUNT \
$MIN_GAS_LIMIT \
$EXTRA_DATA \
$SOURCE_CHAIN_ID \
$DESTINATION_CHAIN_ID \
--rpc-url $L2_RPC_URL \
--private-key $PRIVATE_KEY \
--value $WITHDRAW_AMOUNT 2>&1)
OUTPUT="$TXHASH_OUTPUT"
echo "$OUTPUT"
fi
else
# Withdraw ERC20
log_info "Withdrawing ERC20 token: $L2_TOKEN"
log_info "Amount: $WITHDRAW_AMOUNT"
log_info "From: $WALLET"
log_info "To: $WITHDRAW_TO"
log_info "Source Chain ID: $SOURCE_CHAIN_ID"
log_info "Destination Chain ID: $DESTINATION_CHAIN_ID"
# Check if withdrawing to a different address
if [ "$WITHDRAW_TO" != "$WALLET" ]; then
TXHASH_OUTPUT=$(cast send $L2_STANDARD_BRIDGE_ADDRESS \
"withdrawTo(address,address,uint256,uint32,bytes,uint256,uint256)" \
$L2_TOKEN \
$WITHDRAW_TO \
$WITHDRAW_AMOUNT \
$MIN_GAS_LIMIT \
$EXTRA_DATA \
$SOURCE_CHAIN_ID \
$DESTINATION_CHAIN_ID \
--rpc-url $L2_RPC_URL \
--private-key $PRIVATE_KEY 2>&1)
OUTPUT="$TXHASH_OUTPUT"
echo "$OUTPUT"
else
TXHASH_OUTPUT=$(cast send $L2_STANDARD_BRIDGE_ADDRESS \
"withdraw(address,uint256,uint32,bytes,uint256,uint256)" \
$L2_TOKEN \
$WITHDRAW_AMOUNT \
$MIN_GAS_LIMIT \
$EXTRA_DATA \
$SOURCE_CHAIN_ID \
$DESTINATION_CHAIN_ID \
--rpc-url $L2_RPC_URL \
--private-key $PRIVATE_KEY 2>&1)
OUTPUT="$TXHASH_OUTPUT"
echo "$OUTPUT"
fi
fi
# Try to extract tx hash from cast output and persist to env
# cast send can output in different formats:
# 1. Plain text with transaction hash on a line like "transactionHash 0x..."
# 2. JSON receipt with both transactionHash and blockHash
# We always want transactionHash, never blockHash or log topic hashes
TXHASH=""
BLOCK_HASH=""
# First, try to parse as JSON receipt
if echo "$OUTPUT" | jq -e '.transactionHash' >/dev/null 2>&1; then
# Output is JSON receipt
TXHASH=$(echo "$OUTPUT" | jq -r '.transactionHash' 2>/dev/null)
BLOCK_HASH=$(echo "$OUTPUT" | jq -r '.blockHash // empty' 2>/dev/null)
# Verify we got transactionHash and it's not the same as blockHash
if [ -n "$TXHASH" ] && [ "$TXHASH" != "null" ] && [ "$TXHASH" != "$BLOCK_HASH" ]; then
# Good, we have the transaction hash
:
else
# Failed to get from JSON, try other methods
TXHASH=""
fi
fi
# If JSON parsing failed or didn't work, try plain text extraction
if [ -z "$TXHASH" ] || [ "$TXHASH" = "null" ]; then
# Extract blockHash first (if present) to exclude it
BLOCK_HASH=$(echo "$OUTPUT" | grep -iE '^blockHash\s+' | awk '{for(i=1;i<=NF;i++) if($i ~ /^0x[a-fA-F0-9]{64}$/) {print $i; exit}}' | head -1)
# Look for "transactionHash" field in plain text
# Prioritize lines that START with "transactionHash" (the receipt field)
# Format: "transactionHash 0x..." (field name followed by whitespace and hash)
TXHASH_LINE=$(echo "$OUTPUT" | grep -iE '^transactionHash\s+' | head -1)
if [ -n "$TXHASH_LINE" ]; then
# Extract the hash from the transactionHash line (should be the hex string after the field name)
TXHASH=$(echo "$TXHASH_LINE" | awk '{for(i=1;i<=NF;i++) if($i ~ /^0x[a-fA-F0-9]{64}$/) {print $i; exit}}')
fi
# If not found on a line starting with transactionHash, try any line containing it
# (but this is less reliable as it might match JSON fields in logs)
if [ -z "$TXHASH" ]; then
TXHASH_LINE=$(echo "$OUTPUT" | grep -i 'transactionHash' | grep -v '^logs' | head -1)
if [ -n "$TXHASH_LINE" ]; then
TXHASH=$(echo "$TXHASH_LINE" | awk '{for(i=1;i<=NF;i++) if($i ~ /^0x[a-fA-F0-9]{64}$/) {print $i; exit}}')
fi
fi
fi
# Final validation: ensure we have a valid hash and it's not the block hash
if [ -n "$TXHASH" ] && [ "$TXHASH" != "null" ] && [ ${#TXHASH} -eq 66 ]; then
# Double-check it's not the block hash
if [ -n "$BLOCK_HASH" ] && [ "$TXHASH" = "$BLOCK_HASH" ]; then
log_error "Extracted hash matches blockHash, this is incorrect. Please check the output."
log_info "Output was: $OUTPUT"
return 1
fi
set_env_var "L2_WITHDRAW_TX_HASH" "$TXHASH"
log_info "Saved L2_WITHDRAW_TX_HASH=$TXHASH to $CONFIG_FILE"
# Extract and save L2 block number from the transaction receipt
BLOCK_NUMBER=""
# Try to extract from JSON receipt first
if echo "$OUTPUT" | jq -e '.blockNumber' >/dev/null 2>&1; then
BLOCK_NUMBER=$(echo "$OUTPUT" | jq -r '.blockNumber' 2>/dev/null)
else
# Try to extract from plain text output
BLOCK_NUMBER_LINE=$(echo "$OUTPUT" | grep -iE '^blockNumber\s+' | head -1)
if [ -n "$BLOCK_NUMBER_LINE" ]; then
# Extract the number (could be decimal or hex)
BLOCK_NUMBER=$(echo "$BLOCK_NUMBER_LINE" | awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+$/) {print $i; exit} else if($i ~ /^0x[0-9a-fA-F]+$/) {print $i; exit}}')
fi
fi
if [ -n "$BLOCK_NUMBER" ] && [ "$BLOCK_NUMBER" != "null" ]; then
# Convert to decimal if it's hex
if [[ "$BLOCK_NUMBER" =~ ^0x ]]; then
BLOCK_NUMBER=$(cast --to-dec "$BLOCK_NUMBER" 2>/dev/null || echo "$BLOCK_NUMBER")
fi
set_env_var "L2_BLOCK_NUMBER" "$BLOCK_NUMBER"
log_info "Saved L2_BLOCK_NUMBER=$BLOCK_NUMBER to $CONFIG_FILE"
else
log_warn "Could not extract block number from withdrawal transaction output"
fi
else
log_error "Could not parse transaction hash from withdraw output"
log_info "Output was: $OUTPUT"
return 1
fi
log_verify "Withdrawal transaction sent successfully"
# Reload config to get the newly saved values
source "$CONFIG_FILE"
}
# Build a human-readable summary of the configured withdrawal
summary_withdrawal() {
log_verify "Configured WithdrawalTransaction (L1 view)"
echo " nonce: $WITHDRAWAL_NONCE"
echo " sender: $WITHDRAWAL_SENDER"
echo " target: $WITHDRAWAL_TARGET"
echo " value: $WITHDRAWAL_VALUE"
echo " gasLimit: $WITHDRAWAL_GAS_LIMIT"
echo " destinationChain: ${WITHDRAWAL_DESTINATION_CHAIN_ID:-$L1_CHAIN_IDS}"
echo " data: $WITHDRAWAL_DATA"
}
# Step 4: Extract withdrawal parameters from L2 transaction
# This extracts WITHDRAWAL_NONCE, WITHDRAWAL_SENDER, WITHDRAWAL_TARGET, etc. from the L2 withdrawal transaction
step_4_extract_withdrawal() {
if [ -z "${OPTIMISM_PORTAL_ADDRESS:-}" ]; then
log_error "OPTIMISM_PORTAL_ADDRESS not set in withdraw-config.env (required for L1 steps)"
exit 1
fi
if [ -z "${L2_WITHDRAW_TX_HASH:-}" ]; then
log_error "L2_WITHDRAW_TX_HASH not set in withdraw-config.env"
exit 1
fi
if [ -z "${L2_RPC_URL:-}" ]; then
log_error "L2_RPC_URL not set in withdraw-config.env"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
log_error "jq is required for this script"
exit 1
fi
log_step "Extracting withdrawal parameters from L2 transaction"
echo "Tx: $L2_WITHDRAW_TX_HASH"
echo "RPC: $L2_RPC_URL"
echo ""
# Get the transaction receipt to extract block number
TX_RECEIPT=$(cast receipt "$L2_WITHDRAW_TX_HASH" --rpc-url "$L2_RPC_URL" --json 2>/dev/null)
if [ -z "$TX_RECEIPT" ]; then
log_error "Could not get transaction receipt"
exit 1
fi
# Extract L2 block number from receipt (this is the withdrawal transaction block)
WITHDRAWAL_BLOCK_NUMBER=$(echo "$TX_RECEIPT" | jq -r '.blockNumber' | head -1)
if [ -n "$WITHDRAWAL_BLOCK_NUMBER" ] && [ "$WITHDRAWAL_BLOCK_NUMBER" != "null" ]; then
WITHDRAWAL_BLOCK_NUMBER=$(cast --to-dec "$WITHDRAWAL_BLOCK_NUMBER" 2>/dev/null || echo "$WITHDRAWAL_BLOCK_NUMBER")
set_env_var "WITHDRAWAL_BLOCK_NUMBER" "$WITHDRAWAL_BLOCK_NUMBER"
log_info "Extracted withdrawal transaction block number: $WITHDRAWAL_BLOCK_NUMBER"
else
log_warn "Could not extract block number from transaction receipt"
fi
# Stream the receipt JSON directly into jq to avoid truncation issues
MP_LOG=$(echo "$TX_RECEIPT" | jq -c '
.logs[]
| select(.address|ascii_downcase=="0x4200000000000000000000000000000000000016")
| select(.topics[0]=="0xa9b4c281e35f6150df077bbe47f565aa13fa8cfc04224a49f6526acc577e5aac")
' | head -n1)
if [ -z "$MP_LOG" ] || [ "$MP_LOG" = "null" ]; then
log_error "Could not find MessagePassed log in receipt"
exit 1
fi
# Extract topics and data
TOPIC1=$(echo "$MP_LOG" | jq -r '.topics[1]')
TOPIC2=$(echo "$MP_LOG" | jq -r '.topics[2]')
TOPIC3=$(echo "$MP_LOG" | jq -r '.topics[3]')
DATA_HEX=$(echo "$MP_LOG" | jq -r '.data' | tr 'A-F' 'a-f')
if [ -z "$DATA_HEX" ] || [ "$DATA_HEX" = "null" ]; then
log_error "MessagePassed log missing data field"
exit 1
fi
log_info "Found MessagePassed event in receipt"
log_info "Decoding withdrawal parameters..."
# Decode indexed fields from topics
NONCE=$(cast to-uint256 "$TOPIC1")
SENDER="0x${TOPIC2:26}"
TARGET="0x${TOPIC3:26}"
# Decode non-indexed fields using Python
DECODE_OUT=$(python3 - "$DATA_HEX" <<'PYCODE'
import sys
hexdata = sys.argv[1].strip()
if hexdata.startswith("0x"):
hexdata = hexdata[2:]
def word(i: int) -> str:
start = 64 * i
return hexdata[start:start + 64]
def word_int(i: int) -> int:
return int(word(i), 16)
try:
value = word_int(0)
gaslimit = word_int(1)
offset = word_int(2)
withdraw_hash = "0x" + word(3)
dest_chain_id = word_int(4)
native_coin_amount = word_int(5)
# Offset is in bytes from start; must be word-aligned
if offset % 32 != 0:
raise ValueError("Offset not word-aligned")
bytes_start = offset * 2
bytes_len = int(hexdata[bytes_start:bytes_start + 64], 16)
bytes_data = "0x" + hexdata[bytes_start + 64: bytes_start + 64 + bytes_len * 2]
print(value)
print(gaslimit)
print(bytes_data)
print(withdraw_hash)
print(dest_chain_id)
print(native_coin_amount)
except Exception as e:
print(f"__DECODE_ERROR__ {e}")
sys.exit(1)
PYCODE
)
if [[ "$DECODE_OUT" == __DECODE_ERROR__* ]]; then
log_error "Error decoding data: $DECODE_OUT"
exit 1
fi
mapfile -t PARTS <<<"$DECODE_OUT"
VALUE="${PARTS[0]}"
GASLIMIT="${PARTS[1]}"
BYTES_FIELD="${PARTS[2]}"
WITHDRAW_HASH="${PARTS[3]}"
DEST_CHAIN_ID="${PARTS[4]}"
NATIVE_COIN_AMOUNT="${PARTS[5]}"
# Determine isNativeToERC20 from nativeCoinAmount and value
# isNativeToERC20 = true when nativeCoinAmount == 0 && value > 0 (native-to-ERC20 bridge)
# Otherwise false (standard withdrawals)
IS_NATIVE_TO_ERC20="false"
if [ "$NATIVE_COIN_AMOUNT" = "0" ] && [ "$VALUE" != "0" ]; then
IS_NATIVE_TO_ERC20="true"
fi
log_info "Decoded withdrawal parameters:"
echo " nonce: $NONCE"
echo " sender (L2 XDM): $SENDER"
echo " target (L1 XDM): $TARGET"
echo " value: $VALUE"
echo " gasLimit: $GASLIMIT"
echo " data (relay msg): ${BYTES_FIELD:0:66}..."
echo " withdrawalHash: $WITHDRAW_HASH"
echo " destinationChainId: $DEST_CHAIN_ID"
echo " nativeCoinAmount: $NATIVE_COIN_AMOUNT"
echo " isNativeToERC20: $IS_NATIVE_TO_ERC20"
echo ""
# Save to config file
set_env_var "WITHDRAWAL_NONCE" "$NONCE"
set_env_var "WITHDRAWAL_SENDER" "$SENDER"
set_env_var "WITHDRAWAL_TARGET" "$TARGET"
set_env_var "WITHDRAWAL_VALUE" "$VALUE"
set_env_var "WITHDRAWAL_GAS_LIMIT" "$GASLIMIT"
set_env_var "WITHDRAWAL_DATA" "$BYTES_FIELD"
set_env_var "WITHDRAWAL_DESTINATION_CHAIN_ID" "$DEST_CHAIN_ID"
set_env_var "WITHDRAWAL_HASH" "$WITHDRAW_HASH"
set_env_var "WITHDRAWAL_IS_NATIVE_TO_ERC20" "$IS_NATIVE_TO_ERC20"
# Default PROOF_SUBMITTER to WALLET if not set
DEFAULT_PROOF_SUBMITTER="${PROOF_SUBMITTER:-$WALLET}"
set_env_var "PROOF_SUBMITTER" "$DEFAULT_PROOF_SUBMITTER"
log_success "Withdrawal parameters extracted and saved to $CONFIG_FILE"
# Reload config to get the newly saved values
source "$CONFIG_FILE"
}
# Step 5: Find dispute game for withdrawal
# This finds the game that includes the withdrawal block (game does NOT need to be resolved/finalized)
# You can prove withdrawals even if the game is not yet resolved/finalized
step_5_find_game() {
if [ -z "${L2_WITHDRAW_TX_HASH:-}" ] && [ -z "${WITHDRAWAL_BLOCK_NUMBER:-}" ]; then
log_error "Either L2_WITHDRAW_TX_HASH or WITHDRAWAL_BLOCK_NUMBER must be set"
exit 1
fi
# Get withdrawal block number (where the withdrawal transaction occurred)
if [ -z "${WITHDRAWAL_BLOCK_NUMBER:-}" ]; then
if [ -n "${L2_WITHDRAW_TX_HASH:-}" ]; then
log_info "WITHDRAWAL_BLOCK_NUMBER not set, extracting from withdrawal transaction..."
WITHDRAWAL_BLOCK_NUMBER=$(cast receipt "$L2_WITHDRAW_TX_HASH" --rpc-url "$L2_RPC_URL" --json 2>/dev/null | jq -r '.blockNumber' | head -1)
if [ -n "$WITHDRAWAL_BLOCK_NUMBER" ] && [ "$WITHDRAWAL_BLOCK_NUMBER" != "null" ]; then
WITHDRAWAL_BLOCK_NUMBER=$(cast --to-dec "$WITHDRAWAL_BLOCK_NUMBER" 2>/dev/null || echo "$WITHDRAWAL_BLOCK_NUMBER")
set_env_var "WITHDRAWAL_BLOCK_NUMBER" "$WITHDRAWAL_BLOCK_NUMBER"
log_info "Extracted WITHDRAWAL_BLOCK_NUMBER: $WITHDRAWAL_BLOCK_NUMBER"
else
log_error "Could not get block number from transaction"
exit 1
fi
else
log_error "Either L2_WITHDRAW_TX_HASH or WITHDRAWAL_BLOCK_NUMBER must be set"
exit 1
fi
fi
if [ -z "${DISPUTE_GAME_FACTORY:-}" ]; then
log_error "DISPUTE_GAME_FACTORY not set in withdraw-config.env"
exit 1
fi
if [ -z "${ANCHOR_STATE_REGISTRY:-}" ]; then
log_error "ANCHOR_STATE_REGISTRY not set in withdraw-config.env"
exit 1
fi
if [ -z "${OPTIMISM_PORTAL_ADDRESS:-}" ]; then
log_error "OPTIMISM_PORTAL_ADDRESS not set in withdraw-config.env"
exit 1
fi
log_step "Finding dispute game for withdrawal block $WITHDRAWAL_BLOCK_NUMBER"
log_info "Note: Game does NOT need to be resolved/finalized to prove withdrawal"
echo ""
# Find the game that includes the withdrawal block
GAME_COUNT=$(cast call "$DISPUTE_GAME_FACTORY" "gameCount()(uint256)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
GAME_COUNT_DEC=$(cast --to-dec "$GAME_COUNT" 2>/dev/null || echo "$GAME_COUNT")
if [ -z "$GAME_COUNT" ] || [ "$GAME_COUNT_DEC" = "0" ]; then
log_error "No dispute games found. A dispute game must be created first."
echo ""
echo "Options:"
echo " 1. Wait for the proposer to automatically create a game"
echo " 2. Manually create a game using:"
echo " bash manual-create-game.sh $WITHDRAWAL_BLOCK_NUMBER"
echo ""
echo " Note: The game must be created for block $WITHDRAWAL_BLOCK_NUMBER or later"
echo " to include your withdrawal transaction."
exit 1
fi
RESPECTED_GAME_TYPE=$(cast call "$OPTIMISM_PORTAL_ADDRESS" "respectedGameType()(uint32)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
log_info "Total games: $GAME_COUNT_DEC, Respected game type: $RESPECTED_GAME_TYPE"
TARGET_GAME_INDEX=""
TARGET_GAME_ADDR=""
# Search backwards from most recent to find game that includes withdrawal block
for i in $(seq $((GAME_COUNT_DEC - 1)) -1 0); do
GAME_INFO=$(cast call "$DISPUTE_GAME_FACTORY" "gameAtIndex(uint256)(uint32,uint64,address)" "$i" --rpc-url "$L1_RPC_URL" 2>/dev/null)
if [ -z "$GAME_INFO" ]; then
continue
fi
# Parse game info
GAME_TYPE=$(echo "$GAME_INFO" | head -1 | awk '{print $1}' | tr -d ',')
GAME_ADDR=$(echo "$GAME_INFO" | tail -1 | awk '{print $1}' | tr -d ',')
if [ -z "$GAME_ADDR" ] || [[ ! "$GAME_ADDR" =~ ^0x ]]; then
GAME_ADDR=$(echo "$GAME_INFO" | awk '{print $NF}' | tr -d '[,]')
fi
# Only check respected game type
if [ "$GAME_TYPE" != "$RESPECTED_GAME_TYPE" ]; then
continue
fi
# Get game's L2 block number
GAME_L2_BLOCK=$(cast call "$GAME_ADDR" "l2BlockNumber()(uint256)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
if [ -z "$GAME_L2_BLOCK" ] || [ "$GAME_L2_BLOCK" = "0" ]; then
GAME_L2_BLOCK=$(cast call "$GAME_ADDR" "l2SequenceNumber()(uint256)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
fi
if [ -z "$GAME_L2_BLOCK" ] || [ "$GAME_L2_BLOCK" = "0" ]; then
continue
fi
GAME_L2_BLOCK_DEC=$(cast --to-dec "$GAME_L2_BLOCK" 2>/dev/null || echo "$GAME_L2_BLOCK")
# Check if game includes withdrawal block
if [ "$GAME_L2_BLOCK_DEC" -ge "$WITHDRAWAL_BLOCK_NUMBER" ]; then
TARGET_GAME_INDEX=$i
TARGET_GAME_ADDR=$GAME_ADDR
log_success "Found game at index $i (L2 block: $GAME_L2_BLOCK_DEC) that includes withdrawal block"
break
fi
done
if [ -z "$TARGET_GAME_INDEX" ]; then
log_error "No game found that includes withdrawal block $WITHDRAWAL_BLOCK_NUMBER"
echo ""
echo "Options:"
echo " 1. Wait for proposer to create a game for block $WITHDRAWAL_BLOCK_NUMBER or later"
echo " 2. Manually create a game using:"
echo " bash manual-create-game.sh $WITHDRAWAL_BLOCK_NUMBER"
echo " Then re-run this step (step 5) to resolve and finalize it"
echo ""
echo " Note: The game's L2 block number must be >= $WITHDRAWAL_BLOCK_NUMBER"
echo " to include your withdrawal transaction."
exit 1
fi
echo "Game Address: $TARGET_GAME_ADDR"
echo "Game Index: $TARGET_GAME_INDEX"
echo ""
# Save the game index immediately (so it's saved even if we exit early)
set_env_var "DISPUTE_GAME_INDEX" "$TARGET_GAME_INDEX"
log_info "Saved DISPUTE_GAME_INDEX=$TARGET_GAME_INDEX to $CONFIG_FILE"
# Check current game status (informational only - not required for proving)
GAME_STATUS=$(cast call "$TARGET_GAME_ADDR" "status()(uint8)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
GAME_STATUS_DEC=$(cast --to-dec "$GAME_STATUS" 2>/dev/null || echo "$GAME_STATUS")
IS_FINALIZED=$(cast call "$ANCHOR_STATE_REGISTRY" "isGameFinalized(address)(bool)" "$TARGET_GAME_ADDR" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
echo ""
log_info "Game Status: $GAME_STATUS_DEC (0=IN_PROGRESS, 1=CHALLENGER_WINS, 2=DEFENDER_WINS)"
if [ "$IS_FINALIZED" = "true" ] || [ "$IS_FINALIZED" = "1" ]; then
log_success "Game is finalized (ready for finalization step)"
else
log_info "Game is NOT finalized yet (but you can still prove withdrawal)"
log_info "Run step 9 to resolve and finalize the game before finalizing withdrawal"
fi
# Reload config to get the newly saved value
source "$CONFIG_FILE"
log_success "Dispute game found! You can now proceed to step 6 (get prove parameters)"
log_info "Note: You can prove withdrawal (step 8) even if the game is not resolved/finalized"
log_info " The game only needs to be finalized (step 9) before finalizing withdrawal (step 11)"
}
# Step 6: Get prove parameters (dispute game index and output root proof)
# This gathers all output root proof parameters using the game from step 5
# Note: Game does NOT need to be resolved/finalized to prove withdrawal
step_6_get_prove_params() {
if [ -z "${OPTIMISM_PORTAL_ADDRESS:-}" ]; then
log_error "OPTIMISM_PORTAL_ADDRESS not set in withdraw-config.env (required for L1 steps)"
exit 1
fi
if [ -z "${L2_WITHDRAW_TX_HASH:-}" ] && [ -z "${L2_BLOCK_NUMBER:-}" ]; then
log_error "Either L2_WITHDRAW_TX_HASH or L2_BLOCK_NUMBER must be set"
exit 1
fi
# Get L2 block number from withdrawal transaction if not set
if [ -z "${L2_BLOCK_NUMBER:-}" ]; then
log_info "L2_BLOCK_NUMBER not set, extracting from withdrawal transaction..."
L2_BLOCK_NUMBER=$(cast receipt "$L2_WITHDRAW_TX_HASH" --rpc-url "$L2_RPC_URL" --json 2>/dev/null | jq -r '.blockNumber' | head -1)
if [ -n "$L2_BLOCK_NUMBER" ] && [ "$L2_BLOCK_NUMBER" != "null" ]; then
L2_BLOCK_NUMBER=$(cast --to-dec "$L2_BLOCK_NUMBER" 2>/dev/null || echo "$L2_BLOCK_NUMBER")
set_env_var "L2_BLOCK_NUMBER" "$L2_BLOCK_NUMBER"
log_info "Extracted L2_BLOCK_NUMBER: $L2_BLOCK_NUMBER"
else
log_error "Could not get block number from transaction"
exit 1
fi
fi
if [ -z "${DISPUTE_GAME_FACTORY:-}" ]; then
log_error "DISPUTE_GAME_FACTORY not set in withdraw-config.env"
exit 1
fi
if [ -z "${ANCHOR_STATE_REGISTRY:-}" ]; then
log_error "ANCHOR_STATE_REGISTRY not set in withdraw-config.env"
exit 1
fi
L2_TO_L1_MESSAGE_PASSER="0x4200000000000000000000000000000000000016"
log_step "Gathering parameters for proveWithdrawalTransaction"
log_info "Withdrawal transaction block: $WITHDRAWAL_BLOCK_NUMBER"
echo ""
# Step 6.1: Use dispute game from step 5 (or find it if not set)
if [ -z "${DISPUTE_GAME_INDEX:-}" ]; then
log_error "DISPUTE_GAME_INDEX not set. Run step 5 (find dispute game) first."
exit 1
fi
log_info "Using dispute game index from step 5: $DISPUTE_GAME_INDEX"
# Get game info
GAME_INFO=$(cast call "$DISPUTE_GAME_FACTORY" "gameAtIndex(uint256)(uint32,uint64,address)" "$DISPUTE_GAME_INDEX" --rpc-url "$L1_RPC_URL" 2>/dev/null)
if [ -z "$GAME_INFO" ]; then
log_error "Could not get game at index $DISPUTE_GAME_INDEX"
exit 1
fi
# Parse game info
GAME_TYPE=$(echo "$GAME_INFO" | head -1 | awk '{print $1}' | tr -d ',')
GAME_ADDRESS=$(echo "$GAME_INFO" | tail -1 | awk '{print $1}' | tr -d ',')
if [ -z "$GAME_ADDRESS" ] || [[ ! "$GAME_ADDRESS" =~ ^0x ]]; then
GAME_ADDRESS=$(echo "$GAME_INFO" | awk '{print $NF}' | tr -d '[,]')
fi
if [ -z "$GAME_ADDRESS" ] || [[ ! "$GAME_ADDRESS" =~ ^0x ]]; then
log_error "Invalid game address for index $DISPUTE_GAME_INDEX"
exit 1
fi
log_info "Game Address: $GAME_ADDRESS"
# Get game's L2 block number
GAME_L2_BLOCK=$(cast call "$GAME_ADDRESS" "l2BlockNumber()(uint256)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
if [ -z "$GAME_L2_BLOCK" ] || [ "$GAME_L2_BLOCK" = "0" ]; then
GAME_L2_BLOCK=$(cast call "$GAME_ADDRESS" "l2SequenceNumber()(uint256)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
fi
if [ -z "$GAME_L2_BLOCK" ] || [ "$GAME_L2_BLOCK" = "0" ]; then
log_error "Could not get L2 block number from game $GAME_ADDRESS"
exit 1
fi
GAME_L2_BLOCK_DEC=$(cast --to-dec "$GAME_L2_BLOCK" 2>/dev/null || echo "$GAME_L2_BLOCK")
# IMPORTANT: The OutputRootProof must match the dispute game's rootClaim
# The game was created for a specific L2 block, so we need to use that block's state
log_info "Using game's L2 block ($GAME_L2_BLOCK_DEC) for output root proof (must match game's rootClaim)"
# Step 6.2: Get L2 block header for the GAME's block (not withdrawal block)
log_info "Getting L2 block header for game's block ($GAME_L2_BLOCK_DEC)..."
L2_HEADER_JSON=$(cast block "$GAME_L2_BLOCK_DEC" --rpc-url "$L2_RPC_URL" --json 2>/dev/null)
if [ -z "$L2_HEADER_JSON" ]; then
log_error "Could not get L2 block header for block $GAME_L2_BLOCK_DEC"
exit 1
fi
L2_STATE_ROOT=$(echo "$L2_HEADER_JSON" | jq -r '.stateRoot')
L2_BLOCK_HASH=$(echo "$L2_HEADER_JSON" | jq -r '.hash')
log_info "L2 State Root: $L2_STATE_ROOT"
log_info "L2 Block Hash: $L2_BLOCK_HASH"
# Save output root proof parameters
set_env_var "OUTPUT_ROOT_PROOF_VERSION" "0x0000000000000000000000000000000000000000000000000000000000000000"
set_env_var "OUTPUT_ROOT_PROOF_STATE_ROOT" "$L2_STATE_ROOT"
set_env_var "OUTPUT_ROOT_PROOF_L2_BLOCK_HASH" "$L2_BLOCK_HASH"
# Step 6.3: Get MessagePasser storage root for the GAME's block
log_info "Getting MessagePasser storage root for game's block ($GAME_L2_BLOCK_DEC)..."
STORAGE_PROOF_JSON=$(cast rpc eth_getProof "$L2_TO_L1_MESSAGE_PASSER" "[]" "0x$(printf '%x' $GAME_L2_BLOCK_DEC)" --rpc-url "$L2_RPC_URL" 2>/dev/null)
if [ -z "$STORAGE_PROOF_JSON" ]; then
log_error "Could not get storage proof for block $GAME_L2_BLOCK_DEC"
exit 1
fi
MESSAGE_PASSER_STORAGE_ROOT=$(echo "$STORAGE_PROOF_JSON" | jq -r '.storageHash')
log_info "MessagePasser Storage Root: $MESSAGE_PASSER_STORAGE_ROOT"
# Save message passer storage root
set_env_var "OUTPUT_ROOT_PROOF_MESSAGE_PASSER_STORAGE_ROOT" "$MESSAGE_PASSER_STORAGE_ROOT"
# Verify the output root proof matches the game's rootClaim
log_info "Verifying output root proof matches game's rootClaim..."
GAME_ROOT_CLAIM=$(cast call "$GAME_ADDRESS" "rootClaim()(bytes32)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
log_info "Game Root Claim: $GAME_ROOT_CLAIM"
log_info "Note: Output root proof will be verified on-chain against game's rootClaim"
# Save the game's L2 block separately (for output root proof and withdrawal proof)
# IMPORTANT: Both the output root proof AND the withdrawal proof must use the game's block
set_env_var "GAME_L2_BLOCK_NUMBER" "$GAME_L2_BLOCK_DEC"
log_info "Saved GAME_L2_BLOCK_NUMBER=$GAME_L2_BLOCK_DEC (for output root proof and withdrawal proof)"
log_info "WITHDRAWAL_BLOCK_NUMBER=$WITHDRAWAL_BLOCK_NUMBER (withdrawal transaction block - for reference only)"
log_success "All proveWithdrawalTransaction parameters gathered and saved to $CONFIG_FILE"
# Reload config to get the newly saved values
source "$CONFIG_FILE"
}
# Helper to set or update a key in the env file
set_env_var() {
local key="$1"
local value="$2"
touch "$CONFIG_FILE"
if grep -q "^${key}=" "$CONFIG_FILE"; then
perl -0777 -pe 's/^'"${key}"'=.*/'"${key}"'="'"${value//\//\\/}"'"/m' -i "$CONFIG_FILE"
else
echo "${key}=\"${value}\"" >>"$CONFIG_FILE"
fi
}
# Step 7: Generate withdrawal Merkle proof automatically
# This step always regenerates the WITHDRAWAL_PROOF (overwrites existing value)
step_7_generate_proof() {
log_step "Generating withdrawal Merkle proof"
if [ -n "${WITHDRAWAL_PROOF:-}" ]; then
log_info "WITHDRAWAL_PROOF already exists, will be overwritten with new proof"
fi
# Ensure withdrawal hash is set
if [ -z "${WITHDRAWAL_HASH:-}" ]; then
log_error "WITHDRAWAL_HASH not set. Run step 4 (extract withdrawal) first."
exit 1
fi
# CRITICAL: The withdrawal proof must be generated for the SAME L2 block as the output root proof
# This is the game's L2 block, not the withdrawal transaction's block
# DISPUTE_GAME_INDEX must be set (from step 6) to get the correct L2 block
if [ -z "${DISPUTE_GAME_INDEX:-}" ]; then
log_error "DISPUTE_GAME_INDEX not set. Run step 6 (get prove parameters) first."
log_error "The withdrawal proof must be generated for the same L2 block as the output root proof (the game's block)."
exit 1
fi
if [ -z "${DISPUTE_GAME_FACTORY:-}" ]; then
log_error "DISPUTE_GAME_FACTORY not set in withdraw-config.env"
exit 1
fi
log_info "Getting game's L2 block from dispute game (must match output root proof block)..."
GAME_INFO=$(cast call "$DISPUTE_GAME_FACTORY" "gameAtIndex(uint256)(uint32,uint64,address)" "$DISPUTE_GAME_INDEX" --rpc-url "$L1_RPC_URL" 2>/dev/null)
if [ -z "$GAME_INFO" ]; then
log_error "Could not get game at index $DISPUTE_GAME_INDEX"
exit 1
fi
GAME_ADDR=$(echo "$GAME_INFO" | tail -1 | awk '{print $1}' | tr -d '[,]')
if [ -z "$GAME_ADDR" ] || [[ ! "$GAME_ADDR" =~ ^0x ]]; then
GAME_ADDR=$(echo "$GAME_INFO" | awk '{print $NF}' | tr -d '[,]')
fi
if [ -z "$GAME_ADDR" ] || [[ ! "$GAME_ADDR" =~ ^0x ]]; then
log_error "Invalid game address for index $DISPUTE_GAME_INDEX"
exit 1
fi
GAME_L2_BLOCK=$(cast call "$GAME_ADDR" "l2BlockNumber()(uint256)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
if [ -z "$GAME_L2_BLOCK" ] || [ "$GAME_L2_BLOCK" = "0" ]; then
GAME_L2_BLOCK=$(cast call "$GAME_ADDR" "l2SequenceNumber()(uint256)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
fi
if [ -z "$GAME_L2_BLOCK" ] || [ "$GAME_L2_BLOCK" = "0" ]; then
log_error "Could not get L2 block number from game $GAME_ADDR"
exit 1
fi
GAME_L2_BLOCK_DEC=$(cast --to-dec "$GAME_L2_BLOCK" 2>/dev/null || echo "$GAME_L2_BLOCK")
# IMPORTANT: The withdrawal proof must be generated for the SAME L2 block as the output root proof
# This is the game's L2 block, not the withdrawal transaction's block
# The withdrawal was created at WITHDRAWAL_BLOCK_NUMBER, but we need to prove it exists in the state at GAME_L2_BLOCK_NUMBER
# (the game's block that the output root proof is for)
# Use GAME_L2_BLOCK_NUMBER if available, otherwise get it from the game
if [ -n "${GAME_L2_BLOCK_NUMBER:-}" ]; then
PROOF_BLOCK_NUMBER="$GAME_L2_BLOCK_NUMBER"
log_info "Using GAME_L2_BLOCK_NUMBER from config: $PROOF_BLOCK_NUMBER"
else
PROOF_BLOCK_NUMBER="$GAME_L2_BLOCK_DEC"
set_env_var "GAME_L2_BLOCK_NUMBER" "$PROOF_BLOCK_NUMBER"
log_info "Using game's L2 block: $PROOF_BLOCK_NUMBER (for withdrawal proof - must match output root proof block)"
fi
if [ -n "${WITHDRAWAL_BLOCK_NUMBER:-}" ]; then
log_info "Withdrawal transaction was at block: $WITHDRAWAL_BLOCK_NUMBER (for reference only)"
fi
if [ -z "${L2_RPC_URL:-}" ]; then
log_error "L2_RPC_URL not set"
exit 1
fi
log_info "Withdrawal Hash: $WITHDRAWAL_HASH"
log_info "L2 Block Number for proof: $PROOF_BLOCK_NUMBER (game's block - must match output root proof)"
log_info "L2 RPC URL: $L2_RPC_URL"
# Compute storage slot: keccak256(withdrawalHash || 0x00...00)
log_info "Computing storage slot..."
STORAGE_SLOT=$(cast keccak $(cast --concat-hex "$WITHDRAWAL_HASH" "0x0000000000000000000000000000000000000000000000000000000000000000") 2>/dev/null)
if [ -z "$STORAGE_SLOT" ]; then
log_error "Failed to compute storage slot"
exit 1
fi
log_info "Storage Slot: $STORAGE_SLOT"
# Get Merkle proof using eth_getProof
log_info "Getting Merkle proof from L2 RPC..."
L2_TO_L1_MESSAGE_PASSER="0x4200000000000000000000000000000000000016"
# Build the JSON array for storage slots (must be proper JSON array format)
BLOCK_HEX="0x$(printf '%x' $PROOF_BLOCK_NUMBER)"
STORAGE_SLOTS_JSON="[\"$STORAGE_SLOT\"]"
# Call eth_getProof - cast expects the storage slots as a JSON array string
# Note: cast rpc eth_getProof expects: address, ["slot1","slot2"], block
WITHDRAWAL_PROOF_JSON=$(cast rpc eth_getProof \
"$L2_TO_L1_MESSAGE_PASSER" \
"$STORAGE_SLOTS_JSON" \
"$BLOCK_HEX" \
--rpc-url "$L2_RPC_URL" 2>&1)
# Check for errors in the response
if echo "$WITHDRAWAL_PROOF_JSON" | grep -qi "error\|failed"; then
log_error "RPC call failed or returned error"
log_error "Response: $WITHDRAWAL_PROOF_JSON"
log_error "Make sure L2_RPC_URL is correct and the L2 node supports eth_getProof"
exit 1
fi
if [ -z "$WITHDRAWAL_PROOF_JSON" ]; then
log_error "Empty response from L2 RPC"
log_error "Make sure L2_RPC_URL is correct and the L2 node supports eth_getProof"
exit 1
fi
# Extract proof array and format it
log_info "Extracting proof array from JSON response..."
# Direct extraction from storageProof[0].proof array
WITHDRAWAL_PROOF_ARRAY=$(echo "$WITHDRAWAL_PROOF_JSON" | jq -r '.storageProof[0].proof | .[]' 2>/dev/null | tr '\n' ',' | sed 's/,$//')
if [ -z "$WITHDRAWAL_PROOF_ARRAY" ]; then
log_error "Failed to extract proof from JSON response"
log_error "Proof JSON (first 500 chars): $(echo "$WITHDRAWAL_PROOF_JSON" | head -c 500)"
log_error "Trying alternative extraction method..."
# Alternative: try extracting as array first
PROOF_COUNT=$(echo "$WITHDRAWAL_PROOF_JSON" | jq -r '.storageProof[0].proof | length' 2>/dev/null)
if [ -n "$PROOF_COUNT" ] && [ "$PROOF_COUNT" != "null" ] && [ "$PROOF_COUNT" -gt 0 ]; then
log_info "Found $PROOF_COUNT proof nodes, extracting..."
WITHDRAWAL_PROOF_ARRAY=$(echo "$WITHDRAWAL_PROOF_JSON" | jq -r '.storageProof[0].proof[]' 2>/dev/null | tr '\n' ',' | sed 's/,$//')
fi
if [ -z "$WITHDRAWAL_PROOF_ARRAY" ]; then
log_error "All extraction methods failed"
exit 1
fi
fi
# Count proof nodes
PROOF_COUNT=$(echo "$WITHDRAWAL_PROOF_ARRAY" | tr ',' '\n' | wc -l)
log_success "Withdrawal proof generated successfully"
log_info "Proof contains $PROOF_COUNT nodes"
# Save to config file
set_env_var "WITHDRAWAL_PROOF" "$WITHDRAWAL_PROOF_ARRAY"
log_info "Saved WITHDRAWAL_PROOF to $CONFIG_FILE"
# Also save storage slot for reference
set_env_var "WITHDRAWAL_STORAGE_SLOT" "$STORAGE_SLOT"
# Reload config to get the newly saved values
source "$CONFIG_FILE"
}
# Step 8: Prove withdrawal on L1
# This assumes all parameters have been gathered (steps 4-7)
# Auto-runs steps 4-7 if needed
step_8_prove() {
if [ -z "${OPTIMISM_PORTAL_ADDRESS:-}" ]; then
log_error "OPTIMISM_PORTAL_ADDRESS not set in withdraw-config.env (required for L1 steps)"
exit 1
fi
require_withdrawal_params
# Auto-extract withdrawal params if not set
if [ -z "${WITHDRAWAL_NONCE:-}" ] || [ -z "${WITHDRAWAL_HASH:-}" ]; then
log_info "Withdrawal parameters not set, extracting from L2 transaction..."
step_4_extract_withdrawal
# Reload config to get the newly extracted parameters
source "$CONFIG_FILE"
fi
# Auto-find game if DISPUTE_GAME_INDEX not set
if [ -z "${DISPUTE_GAME_INDEX:-}" ]; then
log_info "Dispute game index not set, finding game automatically..."
step_5_find_game
# Reload config to get the newly found game index
source "$CONFIG_FILE"
fi
# Auto-gather prove parameters if not set
if [ -z "${OUTPUT_ROOT_PROOF_STATE_ROOT:-}" ]; then
log_info "Prove parameters not set, gathering them automatically..."
step_6_get_prove_params
# Reload config to get the newly gathered parameters
source "$CONFIG_FILE"
fi
# Auto-generate withdrawal proof if not set
if [ -z "${WITHDRAWAL_PROOF:-}" ]; then
log_info "WITHDRAWAL_PROOF not set, generating it automatically..."
step_7_generate_proof
# Reload config to get the newly generated WITHDRAWAL_PROOF
source "$CONFIG_FILE"
fi
require_prove_inputs
local dest_chain_id="${WITHDRAWAL_DESTINATION_CHAIN_ID:-$L1_CHAIN_IDS}"
log_step "Proving withdrawal via OptimismPortal2.proveWithdrawalTransaction (Output Root proof path)"
summary_withdrawal
echo " disputeGameIndex: $DISPUTE_GAME_INDEX"
echo " outputRootProof:"
echo " version: $OUTPUT_ROOT_PROOF_VERSION"
echo " stateRoot: $OUTPUT_ROOT_PROOF_STATE_ROOT"
echo " messagePasserStorageRoot: $OUTPUT_ROOT_PROOF_MESSAGE_PASSER_STORAGE_ROOT"
echo " latestBlockhash: $OUTPUT_ROOT_PROOF_L2_BLOCK_HASH"
echo " withdrawalProof (len): $(echo "$WITHDRAWAL_PROOF" | tr ',' '\n' | wc -l) nodes"
echo ""
# Get isNativeToERC20 from config (extracted in step 4)
# Default to false if not set (for backwards compatibility)
local is_native_to_erc20="${WITHDRAWAL_IS_NATIVE_TO_ERC20:-false}"
# Send the prove transaction
# Note: WithdrawalTransaction struct now includes isNativeToERC20 (bool) as the 8th field
TX_OUTPUT=$(cast send "$OPTIMISM_PORTAL_ADDRESS" \
"proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes,uint256,bool),uint256,(bytes32,bytes32,bytes32,bytes32),bytes[])" \
"($WITHDRAWAL_NONCE,$WITHDRAWAL_SENDER,$WITHDRAWAL_TARGET,$WITHDRAWAL_VALUE,$WITHDRAWAL_GAS_LIMIT,$WITHDRAWAL_DATA,$dest_chain_id,$is_native_to_erc20)" \
"$DISPUTE_GAME_INDEX" \
"($OUTPUT_ROOT_PROOF_VERSION,$OUTPUT_ROOT_PROOF_STATE_ROOT,$OUTPUT_ROOT_PROOF_MESSAGE_PASSER_STORAGE_ROOT,$OUTPUT_ROOT_PROOF_L2_BLOCK_HASH)" \
"[$WITHDRAWAL_PROOF]" \
--rpc-url "$L1_RPC_URL" \
--private-key "$PRIVATE_KEY" \
2>&1)
if echo "$TX_OUTPUT" | grep -qi "error\|revert\|failed"; then
log_error "Failed to prove withdrawal"
echo "$TX_OUTPUT"
exit 1
fi
# Extract transaction hash
TX_HASH=$(echo "$TX_OUTPUT" | grep -oE '0x[a-fA-F0-9]{64}' | head -1)
if [ -n "$TX_HASH" ]; then
log_success "Withdrawal proven! Transaction: $TX_HASH"
else
log_success "Withdrawal proven!"
fi
echo ""
echo -e "${CYAN}=== Next Steps: Wait for Proof Maturity ===${NC}"
echo ""
# Get proof maturity delay
MATURITY_DELAY=$(cast call "$OPTIMISM_PORTAL_ADDRESS" \
"proofMaturityDelaySeconds()(uint256)" \
--rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
MATURITY_DELAY_DEC=$(cast --to-dec "$MATURITY_DELAY" 2>/dev/null || echo "$MATURITY_DELAY")
echo "Proof Maturity Delay: $MATURITY_DELAY_DEC seconds"
echo " → Reason: Proving withdrawals requires PROOF_MATURITY_DELAY_SECONDS ($MATURITY_DELAY_DEC seconds) after proof submission"
echo " → This delay ensures the proof has sufficient time to be verified before finalization"
echo ""
echo "You must wait $MATURITY_DELAY_DEC seconds before you can finalize the withdrawal."
echo ""
echo -e "${BLUE} For devnet (Anvil) only, fast-forward time with:${NC}"
echo " cast rpc evm_increaseTime $MATURITY_DELAY_DEC --rpc-url $L1_RPC_URL"
echo " cast rpc evm_mine --rpc-url $L1_RPC_URL"
echo ""
echo -e "${YELLOW} Note: On mainnet, you must wait for the proof maturity delay to elapse naturally.${NC}"
echo ""
echo "After fast-forwarding (devnet) or waiting (mainnet), run step 9 to resolve/finalize game, then step 10 to check status, then step 11 to finalize:"
echo " bash withdraw-finalize.sh 9 # Resolve and finalize dispute game"
echo " bash withdraw-finalize.sh 10 # Check withdrawal status"
echo " bash withdraw-finalize.sh 11 # Finalize withdrawal"
echo ""
}
# Helper function: Require all prove inputs to be set
# Requires:
# DISPUTE_GAME_INDEX
# OUTPUT_ROOT_PROOF_VERSION
# OUTPUT_ROOT_PROOF_STATE_ROOT
# OUTPUT_ROOT_PROOF_MESSAGE_PASSER_STORAGE_ROOT
# OUTPUT_ROOT_PROOF_L2_BLOCK_HASH
# WITHDRAWAL_PROOF (comma-separated bytes32 array, e.g. "0xabc...,0xdef...")
require_prove_inputs() {
local missing=0
for var in DISPUTE_GAME_INDEX OUTPUT_ROOT_PROOF_VERSION OUTPUT_ROOT_PROOF_STATE_ROOT OUTPUT_ROOT_PROOF_MESSAGE_PASSER_STORAGE_ROOT OUTPUT_ROOT_PROOF_L2_BLOCK_HASH WITHDRAWAL_PROOF; do
if [ -z "${!var:-}" ]; then
log_error "$var is not set. Please set it in withdraw-config.env"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
exit 1
fi
}
# Step 9: Resolve and finalize dispute game
# This resolves and finalizes the dispute game found in step 5
# Note: Game must be finalized before you can finalize the withdrawal (step 11)
step_9_resolve_game() {
if [ -z "${DISPUTE_GAME_INDEX:-}" ]; then
log_error "DISPUTE_GAME_INDEX not set. Run step 5 (find dispute game) first."
exit 1
fi
if [ -z "${DISPUTE_GAME_FACTORY:-}" ]; then
log_error "DISPUTE_GAME_FACTORY not set in withdraw-config.env"
exit 1
fi
if [ -z "${ANCHOR_STATE_REGISTRY:-}" ]; then
log_error "ANCHOR_STATE_REGISTRY not set in withdraw-config.env"
exit 1
fi
log_step "Resolving and finalizing dispute game (index: $DISPUTE_GAME_INDEX)"
echo ""
# Get game address
GAME_INFO=$(cast call "$DISPUTE_GAME_FACTORY" "gameAtIndex(uint256)(uint32,uint64,address)" "$DISPUTE_GAME_INDEX" --rpc-url "$L1_RPC_URL" 2>/dev/null)
if [ -z "$GAME_INFO" ]; then
log_error "Could not get game at index $DISPUTE_GAME_INDEX"
exit 1
fi
TARGET_GAME_ADDR=$(echo "$GAME_INFO" | tail -1 | awk '{print $1}' | tr -d ',')
if [ -z "$TARGET_GAME_ADDR" ] || [[ ! "$TARGET_GAME_ADDR" =~ ^0x ]]; then
TARGET_GAME_ADDR=$(echo "$GAME_INFO" | awk '{print $NF}' | tr -d '[,]')
fi
if [ -z "$TARGET_GAME_ADDR" ] || [[ ! "$TARGET_GAME_ADDR" =~ ^0x ]]; then
log_error "Invalid game address for index $DISPUTE_GAME_INDEX"
exit 1
fi
echo "Game Address: $TARGET_GAME_ADDR"
echo "Game Index: $DISPUTE_GAME_INDEX"
echo ""
# Check current status
GAME_STATUS=$(cast call "$TARGET_GAME_ADDR" "status()(uint8)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
GAME_STATUS_DEC=$(cast --to-dec "$GAME_STATUS" 2>/dev/null || echo "$GAME_STATUS")
IS_FINALIZED=$(cast call "$ANCHOR_STATE_REGISTRY" "isGameFinalized(address)(bool)" "$TARGET_GAME_ADDR" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
if [ "$IS_FINALIZED" = "true" ] || [ "$IS_FINALIZED" = "1" ]; then
log_success "Game is already finalized! No action needed."
return 0
fi
# Resolve game if needed
if [ "$GAME_STATUS_DEC" = "0" ]; then
log_info "Game is IN_PROGRESS. Resolving game..."
# Get MAX_CLOCK_DURATION
MAX_CLOCK=$(cast call "$TARGET_GAME_ADDR" "maxClockDuration()(uint64)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
MAX_CLOCK_DEC=$(cast --to-dec "$MAX_CLOCK" 2>/dev/null || echo "$MAX_CLOCK")
if [ -z "$MAX_CLOCK_DEC" ] || [ "$MAX_CLOCK_DEC" = "0" ]; then
log_error "Could not get MAX_CLOCK_DURATION for game $TARGET_GAME_ADDR"
exit 1
fi
log_info "MAX_CLOCK_DURATION: $MAX_CLOCK_DEC seconds"
# Check if clock has expired by checking challenger duration for root claim (index 0)
log_info "Checking if clock has expired..."
CHALLENGER_DURATION=$(cast call "$TARGET_GAME_ADDR" "getChallengerDuration(uint256)(uint64)" "0" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
CHALLENGER_DURATION_DEC=$(cast --to-dec "$CHALLENGER_DURATION" 2>/dev/null || echo "$CHALLENGER_DURATION")
if [ -z "$CHALLENGER_DURATION_DEC" ]; then
log_error "Could not get challenger duration for game $TARGET_GAME_ADDR"
exit 1
fi
log_info "Current challenger duration: $CHALLENGER_DURATION_DEC seconds"
log_info "Required (MAX_CLOCK_DURATION): $MAX_CLOCK_DEC seconds"
# Check if clock has expired
if [ "$CHALLENGER_DURATION_DEC" -lt "$MAX_CLOCK_DEC" ]; then
TIME_NEEDED=$((MAX_CLOCK_DEC - CHALLENGER_DURATION_DEC))
log_warn "Clock has NOT expired yet. Need $TIME_NEEDED more seconds."
echo " → Reason: Resolving games requires MAX_CLOCK_DURATION ($MAX_CLOCK_DEC seconds) to expire"
echo " → This is the time limit for challenges before a game can be resolved"
echo ""
echo -e "${BLUE} For devnet (Anvil) only, fast-forward time with:${NC}"
echo " cast rpc evm_increaseTime $TIME_NEEDED --rpc-url $L1_RPC_URL"
echo " cast rpc evm_mine --rpc-url $L1_RPC_URL"
echo ""
echo -e "${YELLOW} Note: On mainnet, you must wait for the clock to expire naturally.${NC}"
echo -e "${YELLOW} After fast-forwarding (devnet) or waiting (mainnet), re-run this step.${NC}"
exit 1
fi
log_success "Clock has expired! Proceeding with game resolution..."
# Resolve the root claim
log_info "Resolving root claim (index 0)..."
cast send "$TARGET_GAME_ADDR" "resolveClaim(uint256,uint256)" "0" "0" \
--rpc-url "$L1_RPC_URL" \
--private-key "$PRIVATE_KEY" >/dev/null 2>&1 || {
log_warn "Failed to resolve root claim (may already be resolved)"
}
log_success "Root claim resolved."
# Resolve the game
log_info "Resolving the game..."
cast send "$TARGET_GAME_ADDR" "resolve()" \
--rpc-url "$L1_RPC_URL" \
--private-key "$PRIVATE_KEY" >/dev/null 2>&1 || {
log_warn "Failed to resolve game (may already be resolved)"
}
log_success "Game resolved."
# Verify resolution
sleep 1
GAME_STATUS=$(cast call "$TARGET_GAME_ADDR" "status()(uint8)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
GAME_STATUS_DEC=$(cast --to-dec "$GAME_STATUS" 2>/dev/null || echo "$GAME_STATUS")
if [ "$GAME_STATUS_DEC" = "2" ]; then
log_success "Game resolved: DEFENDER_WINS ✓"
elif [ "$GAME_STATUS_DEC" = "1" ]; then
log_warn "Game resolved: CHALLENGER_WINS (cannot be used for withdrawals)"
exit 1
else
log_warn "Game resolution may have failed (status = $GAME_STATUS_DEC)"
fi
elif [ "$GAME_STATUS_DEC" = "2" ]; then
log_success "Game is already resolved (DEFENDER_WINS)"
else
log_warn "Game status is $GAME_STATUS_DEC (not DEFENDER_WINS). Cannot be used for withdrawals."
exit 1
fi
# Check finalization
RESOLVED_AT=$(cast call "$TARGET_GAME_ADDR" "resolvedAt()(uint64)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
RESOLVED_AT_DEC=$(cast --to-dec "$RESOLVED_AT" 2>/dev/null || echo "$RESOLVED_AT")
if [ "$RESOLVED_AT_DEC" != "0" ] && [ -n "$RESOLVED_AT_DEC" ]; then
# Get finality delay
FINALITY_DELAY=$(cast call "$ANCHOR_STATE_REGISTRY" "disputeGameFinalityDelaySeconds()(uint256)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
FINALITY_DELAY_DEC=$(cast --to-dec "$FINALITY_DELAY" 2>/dev/null || echo "$FINALITY_DELAY")
if [ -z "$FINALITY_DELAY_DEC" ] || [ "$FINALITY_DELAY_DEC" = "0" ]; then
log_error "Could not get DISPUTE_GAME_FINALITY_DELAY_SECONDS from AnchorStateRegistry"
exit 1
fi
log_info "DISPUTE_GAME_FINALITY_DELAY_SECONDS: $FINALITY_DELAY_DEC seconds"
# Get current timestamp
CURRENT_BLOCK=$(cast block-number --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
CURRENT_TIMESTAMP=$(cast block "$CURRENT_BLOCK" --rpc-url "$L1_RPC_URL" --json 2>/dev/null | jq -r '.timestamp' | head -1)
CURRENT_TIMESTAMP_DEC=$(cast --to-dec "$CURRENT_TIMESTAMP" 2>/dev/null || echo "$CURRENT_TIMESTAMP")
if [ -n "$CURRENT_TIMESTAMP_DEC" ] && [ "$CURRENT_TIMESTAMP_DEC" != "0" ]; then
TIME_ELAPSED=$((CURRENT_TIMESTAMP_DEC - RESOLVED_AT_DEC))
TIME_REMAINING=$((FINALITY_DELAY_DEC - TIME_ELAPSED))
echo "Resolved at: $RESOLVED_AT_DEC"
echo "Current time: $CURRENT_TIMESTAMP_DEC"
echo "Time elapsed: $TIME_ELAPSED seconds"
if [ "$TIME_REMAINING" -gt 0 ]; then
log_warn "Finalization delay not yet elapsed"
echo " → Reason: Finalizing games requires DISPUTE_GAME_FINALITY_DELAY_SECONDS ($FINALITY_DELAY_DEC seconds) after resolution"
echo " → This delay ensures the game outcome is settled before it can be used for withdrawals"
echo ""
echo " Time remaining: $TIME_REMAINING seconds"
echo ""
echo -e "${BLUE} For devnet (Anvil) only, fast-forward time with:${NC}"
echo " cast rpc evm_increaseTime $TIME_REMAINING --rpc-url $L1_RPC_URL"
echo " cast rpc evm_mine --rpc-url $L1_RPC_URL"
echo ""
echo -e "${YELLOW} Note: On mainnet, you must wait for the finalization delay to elapse naturally.${NC}"
echo -e "${YELLOW} After fast-forwarding (devnet) or waiting (mainnet), re-run this step.${NC}"
exit 1
fi
# Verify finalization
IS_FINALIZED=$(cast call "$ANCHOR_STATE_REGISTRY" "isGameFinalized(address)(bool)" "$TARGET_GAME_ADDR" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
if [ "$IS_FINALIZED" = "true" ] || [ "$IS_FINALIZED" = "1" ]; then
echo ""
log_success "✓ Game IS FINALIZED!"
echo "This game can now be used for finalizing withdrawals!"
else
log_warn "Game is not finalized yet (may need to check isGameProper/isGameClaimValid)"
fi
fi
else
log_warn "Game is not resolved yet (resolvedAt = 0)"
fi
}
# Step 10: Check if withdrawal has been proven and is ready to finalize
# Requires WITHDRAWAL_HASH and PROOF_SUBMITTER (address that called proveWithdrawalTransaction)
step_10_check_status() {
if [ -z "${OPTIMISM_PORTAL_ADDRESS:-}" ]; then
log_error "OPTIMISM_PORTAL_ADDRESS not set in withdraw-config.env (required for L1 steps)"
exit 1
fi
if [ -z "${WITHDRAWAL_HASH:-}" ]; then
log_error "WITHDRAWAL_HASH not set (hashWithdrawal result). Set it in withdraw-config.env"
exit 1
fi
# Auto-detect PROOF_SUBMITTER from PRIVATE_KEY if not set
if [ -z "${PROOF_SUBMITTER:-}" ]; then
if [ -n "${PRIVATE_KEY:-}" ]; then
PROOF_SUBMITTER=$(cast wallet address "$PRIVATE_KEY" 2>/dev/null || echo "")
if [ -n "$PROOF_SUBMITTER" ]; then
log_info "Auto-detected PROOF_SUBMITTER from PRIVATE_KEY: $PROOF_SUBMITTER"
fi
fi
if [ -z "${PROOF_SUBMITTER:-}" ]; then
log_error "PROOF_SUBMITTER not set. Set it in withdraw-config.env or ensure PRIVATE_KEY is set."
exit 1
fi
fi
log_step "Checking withdrawal status on OptimismPortal2"
echo "Portal: $OPTIMISM_PORTAL_ADDRESS"
echo "Withdrawal hash: $WITHDRAWAL_HASH"
echo "Proof submitter: $PROOF_SUBMITTER"
echo ""
# 1) Check if already finalized
FINALIZED=$(cast call "$OPTIMISM_PORTAL_ADDRESS" \
"finalizedWithdrawals(bytes32)(bool)" \
"$WITHDRAWAL_HASH" \
--rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
if [ "$FINALIZED" = "true" ] || [ "$FINALIZED" = "1" ]; then
echo -e "${GREEN}✓ Withdrawal already finalized on L1${NC}"
return 0
else
echo -e "${YELLOW}⚠ Withdrawal not yet finalized on L1${NC}"
fi
# 2) Check if withdrawal is proven
log_info "Checking if withdrawal is proven..."
PROVEN_DATA=$(cast call "$OPTIMISM_PORTAL_ADDRESS" \
"provenWithdrawals(bytes32,address)(address,uint64)" \
"$WITHDRAWAL_HASH" \
"$PROOF_SUBMITTER" \
--rpc-url "$L1_RPC_URL" 2>/dev/null)
PROVEN_TIMESTAMP=$(echo "$PROVEN_DATA" | tail -1 | awk '{print $1}' | tr -d '[,]')
PROVEN_TIMESTAMP_DEC=$(cast --to-dec "$PROVEN_TIMESTAMP" 2>/dev/null || echo "$PROVEN_TIMESTAMP")
if [ -z "$PROVEN_TIMESTAMP_DEC" ] || [ "$PROVEN_TIMESTAMP_DEC" = "0" ]; then
echo -e "${RED}✗ Withdrawal has NOT been proven yet${NC}"
echo " You need to run step 8 (prove withdrawal) first."
return 1
fi
echo -e "${GREEN}✓ Withdrawal is proven${NC}"
echo " Proof submitted at timestamp: $PROVEN_TIMESTAMP_DEC"
# 3) Get proof maturity delay
MATURITY_DELAY=$(cast call "$OPTIMISM_PORTAL_ADDRESS" \
"proofMaturityDelaySeconds()(uint256)" \
--rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
MATURITY_DELAY_DEC=$(cast --to-dec "$MATURITY_DELAY" 2>/dev/null || echo "$MATURITY_DELAY")
echo " Proof maturity delay: $MATURITY_DELAY_DEC seconds"
echo " → Reason: Proving withdrawals requires PROOF_MATURITY_DELAY_SECONDS ($MATURITY_DELAY_DEC seconds) after proof submission"
echo " → This delay ensures the proof has sufficient time to be verified before finalization"
echo ""
# 4) Get current block timestamp
CURRENT_BLOCK=$(cast block-number --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
CURRENT_TIMESTAMP=$(cast block "$CURRENT_BLOCK" --rpc-url "$L1_RPC_URL" --json 2>/dev/null | jq -r '.timestamp' | head -1)
CURRENT_TIMESTAMP_DEC=$(cast --to-dec "$CURRENT_TIMESTAMP" 2>/dev/null || echo "$CURRENT_TIMESTAMP")
# 5) Calculate time elapsed and remaining
TIME_ELAPSED=$((CURRENT_TIMESTAMP_DEC - PROVEN_TIMESTAMP_DEC))
TIME_REMAINING=$((MATURITY_DELAY_DEC - TIME_ELAPSED))
echo " Current timestamp: $CURRENT_TIMESTAMP_DEC"
echo " Time elapsed since proof: $TIME_ELAPSED seconds"
echo ""
if [ "$TIME_REMAINING" -gt 0 ]; then
REMAINING_MINUTES=$(echo "scale=1; $TIME_REMAINING / 60" | bc 2>/dev/null || echo "N/A")
echo -e "${YELLOW}⚠ Proof maturity delay not yet elapsed${NC}"
echo " Time remaining: $TIME_REMAINING seconds ($REMAINING_MINUTES minutes)"
echo ""
echo " To proceed, you need to wait $TIME_REMAINING seconds."
echo ""
echo -e "${BLUE} For devnet (Anvil) only, fast-forward time with:${NC}"
echo " cast rpc evm_increaseTime $TIME_REMAINING --rpc-url $L1_RPC_URL"
echo " cast rpc evm_mine --rpc-url $L1_RPC_URL"
echo ""
echo -e "${YELLOW} Note: On mainnet, you must wait for the proof maturity delay to elapse naturally.${NC}"
echo -e "${YELLOW} After fast-forwarding (devnet) or waiting (mainnet), re-run this step.${NC}"
else
echo -e "${GREEN}✓ Proof maturity delay has elapsed${NC}"
echo ""
# 6) Final check with checkWithdrawal
log_verify "Calling checkWithdrawal (final verification)"
set +e
CHECK_OUTPUT=$(cast call "$OPTIMISM_PORTAL_ADDRESS" \
"checkWithdrawal(bytes32,address)" \
"$WITHDRAWAL_HASH" \
"$PROOF_SUBMITTER" \
--rpc-url "$L1_RPC_URL" 2>&1)
STATUS=$?
set -e
if [ "$STATUS" -eq 0 ]; then
echo -e "${GREEN}✓ Withdrawal is ready to finalize!${NC}"
echo " You can now run step 11 to finalize withdrawal"
else
echo -e "${YELLOW}⚠ checkWithdrawal reverted${NC}"
echo " Error output: $CHECK_OUTPUT"
echo ""
echo " Possible reasons:"
echo " - Dispute game not finalized or invalid"
echo " - Game root claim invalid"
echo " - Other validation failed"
fi
fi
}
# Step 11: Finalize withdrawal on L1
# This assumes:
# - You already called proveWithdrawalTransaction (with the same PRIVATE_KEY / PROOF_SUBMITTER)
# - The dispute game has been finalized (step 9)
# - checkWithdrawal passes (step 10)
step_11_finalize() {
if [ -z "${OPTIMISM_PORTAL_ADDRESS:-}" ]; then
log_error "OPTIMISM_PORTAL_ADDRESS not set in withdraw-config.env (required for L1 steps)"
exit 1
fi
require_withdrawal_params
local dest_chain_id="${WITHDRAWAL_DESTINATION_CHAIN_ID:-$L1_CHAIN_IDS}"
log_step "Finalizing withdrawal via OptimismPortal2.finalizeWithdrawalTransaction"
summary_withdrawal
log_info "Sending finalizeWithdrawalTransaction tx from $WALLET (PRIVATE_KEY) to $OPTIMISM_PORTAL_ADDRESS"
# Get isNativeToERC20 from config (extracted in step 4)
# Default to false if not set (for backwards compatibility)
local is_native_to_erc20="${WITHDRAWAL_IS_NATIVE_TO_ERC20:-false}"
# Capture transaction output to check for errors
# Note: WithdrawalTransaction struct now includes isNativeToERC20 (bool) as the 8th field
TX_OUTPUT=$(cast send "$OPTIMISM_PORTAL_ADDRESS" \
"finalizeWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes,uint256,bool))" \
"($WITHDRAWAL_NONCE,$WITHDRAWAL_SENDER,$WITHDRAWAL_TARGET,$WITHDRAWAL_VALUE,$WITHDRAWAL_GAS_LIMIT,$WITHDRAWAL_DATA,$dest_chain_id,$is_native_to_erc20)" \
--rpc-url "$L1_RPC_URL" \
--private-key "$PRIVATE_KEY" \
2>&1)
# Display the output
echo "$TX_OUTPUT"
# Extract transaction hash and output for error checking
TX_HASH=$(echo "$TX_OUTPUT" | grep -oE '0x[a-fA-F0-9]{64}' | head -1)
# If no hash found in output, try to get it from receipt (transaction might have succeeded)
if [ -z "$TX_HASH" ]; then
# Try to extract from any transaction hash pattern in the output
TX_HASH=$(echo "$TX_OUTPUT" | grep -iE 'transaction.*hash|tx.*hash|hash:' | grep -oE '0x[a-fA-F0-9]{64}' | head -1)
fi
# Check for specific errors
if echo "$TX_OUTPUT" | grep -qi "OptimismPortal_ProofNotOldEnough\|ProofNotOldEnough\|0xd9bc01be"; then
log_error "Proof maturity delay has not elapsed yet"
echo ""
echo " → Reason: Finalizing withdrawals requires PROOF_MATURITY_DELAY_SECONDS to elapse after proof submission"
echo " → This delay ensures the proof has sufficient time to be verified before finalization"
echo ""
# Get proof maturity delay and check time remaining
if [ -n "${WITHDRAWAL_HASH:-}" ] && [ -n "${PROOF_SUBMITTER:-}" ]; then
MATURITY_DELAY=$(cast call "$OPTIMISM_PORTAL_ADDRESS" \
"proofMaturityDelaySeconds()(uint256)" \
--rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
MATURITY_DELAY_DEC=$(cast --to-dec "$MATURITY_DELAY" 2>/dev/null || echo "$MATURITY_DELAY")
PROVEN_DATA=$(cast call "$OPTIMISM_PORTAL_ADDRESS" \
"provenWithdrawals(bytes32,address)(address,uint64)" \
"$WITHDRAWAL_HASH" \
"$PROOF_SUBMITTER" \
--rpc-url "$L1_RPC_URL" 2>/dev/null)
PROVEN_TIMESTAMP=$(echo "$PROVEN_DATA" | tail -1 | awk '{print $1}' | tr -d '[,]')
PROVEN_TIMESTAMP_DEC=$(cast --to-dec "$PROVEN_TIMESTAMP" 2>/dev/null || echo "$PROVEN_TIMESTAMP")
if [ -n "$PROVEN_TIMESTAMP_DEC" ] && [ "$PROVEN_TIMESTAMP_DEC" != "0" ]; then
CURRENT_BLOCK=$(cast block-number --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
CURRENT_TIMESTAMP=$(cast block "$CURRENT_BLOCK" --rpc-url "$L1_RPC_URL" --json 2>/dev/null | jq -r '.timestamp' | head -1)
CURRENT_TIMESTAMP_DEC=$(cast --to-dec "$CURRENT_TIMESTAMP" 2>/dev/null || echo "$CURRENT_TIMESTAMP")
TIME_ELAPSED=$((CURRENT_TIMESTAMP_DEC - PROVEN_TIMESTAMP_DEC))
TIME_REMAINING=$((MATURITY_DELAY_DEC - TIME_ELAPSED))
echo " Proof submitted at: $PROVEN_TIMESTAMP_DEC"
echo " Current time: $CURRENT_TIMESTAMP_DEC"
echo " Time elapsed: $TIME_ELAPSED seconds"
echo " Proof maturity delay: $MATURITY_DELAY_DEC seconds"
echo ""
if [ "$TIME_REMAINING" -gt 0 ]; then
REMAINING_MINUTES=$(echo "scale=1; $TIME_REMAINING / 60" | bc 2>/dev/null || echo "N/A")
echo " Time remaining: $TIME_REMAINING seconds ($REMAINING_MINUTES minutes)"
echo ""
echo -e "${BLUE} For devnet (Anvil) only, fast-forward time with:${NC}"
echo " cast rpc evm_increaseTime $TIME_REMAINING --rpc-url $L1_RPC_URL"
echo " cast rpc evm_mine --rpc-url $L1_RPC_URL"
echo ""
echo -e "${YELLOW} Note: On mainnet, you must wait for the proof maturity delay to elapse naturally.${NC}"
echo " After fast-forwarding (devnet) or waiting (mainnet), re-run step 10."
else
echo " Proof maturity delay should have elapsed. Check step 10 for details."
fi
fi
else
echo " Run step 10 (check withdrawal status) to see the exact time remaining."
fi
exit 1
elif echo "$TX_OUTPUT" | grep -qi "error\|revert\|failed"; then
log_error "Failed to finalize withdrawal"
echo "$TX_OUTPUT"
exit 1
else
# Success - TX_HASH was already extracted above
if [ -n "$TX_HASH" ]; then
log_success "Withdrawal finalized! Transaction: $TX_HASH"
else
log_success "Withdrawal finalized!"
# Try to extract from output one more time
TX_HASH=$(echo "$TX_OUTPUT" | grep -oE '0x[a-fA-F0-9]{64}' | head -1)
if [ -n "$TX_HASH" ]; then
echo "Transaction hash: $TX_HASH"
fi
fi
echo ""
echo "You can now check your L1 balances with step 12."
fi
}
# Step 12: Sanity checks on L1 balances after finalization
step_12_check_l1_balances() {
log_step "Checking L1 balances after (or before) finalization"
TARGET_ADDR="${WITHDRAW_TO:-$WALLET}"
# Check if L1_TOKEN is zero address or empty, or if WITHDRAW_ETH is true
if [ "$WITHDRAW_ETH" = "true" ] || [ -z "${L1_TOKEN:-}" ] || [ "$L1_TOKEN" = "0x0000000000000000000000000000000000000000" ]; then
log_verify "L1 ETH balance for $TARGET_ADDR"
BALANCE_WEI=$(cast balance "$TARGET_ADDR" --rpc-url "$L1_RPC_URL" 2>/dev/null)
if [ -n "$BALANCE_WEI" ] && [ "$BALANCE_WEI" != "0" ]; then
BALANCE_DEC=$(cast --to-dec "$BALANCE_WEI" 2>/dev/null || echo "$BALANCE_WEI")
BALANCE_DEC=$(echo "$BALANCE_DEC" | sed 's/\[.*\]//' | tr -d ' ')
if command -v bc &> /dev/null; then
BALANCE_ETH=$(echo "scale=18; $BALANCE_DEC / (10^18)" | bc 2>/dev/null)
echo "$BALANCE_WEI wei = $BALANCE_ETH ETH"
else
echo "$BALANCE_WEI wei"
fi
else
echo "$BALANCE_WEI wei"
fi
else
log_verify "L1 token balance for $TARGET_ADDR"
BALANCE_RAW=$(cast call "$L1_TOKEN" "balanceOf(address)(uint256)" "$TARGET_ADDR" --rpc-url "$L1_RPC_URL" 2>/dev/null)
if [ -n "$BALANCE_RAW" ] && [ "$BALANCE_RAW" != "0" ]; then
# Get token decimals
DECIMALS=$(cast call "$L1_TOKEN" "decimals()(uint8)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
if [ -z "$DECIMALS" ] || [ "$DECIMALS" = "null" ]; then
DECIMALS="18" # Default to 18 if not found
fi
DECIMALS=$(cast --to-dec "$DECIMALS" 2>/dev/null || echo "$DECIMALS")
DECIMALS=$(echo "$DECIMALS" | sed 's/\[.*\]//' | tr -d ' ')
BALANCE_DEC=$(cast --to-dec "$BALANCE_RAW" 2>/dev/null || echo "$BALANCE_RAW")
BALANCE_DEC=$(echo "$BALANCE_DEC" | sed 's/\[.*\]//' | tr -d ' ')
if command -v bc &> /dev/null; then
BALANCE_HUMAN=$(echo "scale=$DECIMALS; $BALANCE_DEC / (10^$DECIMALS)" | bc 2>/dev/null)
echo "$BALANCE_RAW (raw) = $BALANCE_HUMAN tokens (with $DECIMALS decimals)"
else
echo "$BALANCE_RAW (raw)"
fi
else
echo "$BALANCE_RAW (raw)"
fi
fi
# Check ETH lockbox balance instead of L1 Standard Bridge
if [ -n "${L1_ETH_LOCK_BOX_ADDRESS:-}" ] && [ "$L1_ETH_LOCK_BOX_ADDRESS" != "0x0000000000000000000000000000000000000000" ]; then
if [ "$WITHDRAW_ETH" = "true" ] || [ -z "${L1_TOKEN:-}" ] || [ "$L1_TOKEN" = "0x0000000000000000000000000000000000000000" ]; then
log_verify "ETH Lockbox balance (native ETH)"
BALANCE_WEI=$(cast balance "$L1_ETH_LOCK_BOX_ADDRESS" --rpc-url "$L1_RPC_URL" 2>/dev/null)
if [ -n "$BALANCE_WEI" ] && [ "$BALANCE_WEI" != "0" ]; then
BALANCE_DEC=$(cast --to-dec "$BALANCE_WEI" 2>/dev/null || echo "$BALANCE_WEI")
BALANCE_DEC=$(echo "$BALANCE_DEC" | sed 's/\[.*\]//' | tr -d ' ')
if command -v bc &> /dev/null; then
BALANCE_ETH=$(echo "scale=18; $BALANCE_DEC / (10^18)" | bc 2>/dev/null)
echo "$BALANCE_WEI wei = $BALANCE_ETH ETH"
else
echo "$BALANCE_WEI wei"
fi
else
echo "$BALANCE_WEI wei"
fi
else
log_verify "ETH Lockbox balance of L1 token"
BALANCE_RAW=$(cast call "$L1_TOKEN" "balanceOf(address)(uint256)" "$L1_ETH_LOCK_BOX_ADDRESS" --rpc-url "$L1_RPC_URL" 2>/dev/null)
if [ -n "$BALANCE_RAW" ] && [ "$BALANCE_RAW" != "0" ]; then
# Get token decimals
DECIMALS=$(cast call "$L1_TOKEN" "decimals()(uint8)" --rpc-url "$L1_RPC_URL" 2>/dev/null | head -1 | awk '{print $1}' | tr -d '[,]')
if [ -z "$DECIMALS" ] || [ "$DECIMALS" = "null" ]; then
DECIMALS="18" # Default to 18 if not found
fi
DECIMALS=$(cast --to-dec "$DECIMALS" 2>/dev/null || echo "$DECIMALS")
DECIMALS=$(echo "$DECIMALS" | sed 's/\[.*\]//' | tr -d ' ')
BALANCE_DEC=$(cast --to-dec "$BALANCE_RAW" 2>/dev/null || echo "$BALANCE_RAW")
BALANCE_DEC=$(echo "$BALANCE_DEC" | sed 's/\[.*\]//' | tr -d ' ')
if command -v bc &> /dev/null; then
BALANCE_HUMAN=$(echo "scale=$DECIMALS; $BALANCE_DEC / (10^$DECIMALS)" | bc 2>/dev/null)
echo "$BALANCE_RAW (raw) = $BALANCE_HUMAN tokens (with $DECIMALS decimals)"
else
echo "$BALANCE_RAW (raw)"
fi
else
echo "$BALANCE_RAW (raw)"
fi
fi
fi
}
show_menu() {
echo ""
echo "=== Withdraw Finalization Tool (Complete Withdrawal Flow) ==="
echo "0. Set QNT Token L1 on L2 TokenWhitelist"
echo "1. Check L2 balance (ETH / token)"
echo "2. Approve L2 Standard Bridge (ERC20 only)"
echo "3. Withdraw tokens from L2 to L1"
echo "4. Extract withdrawal parameters from L2 transaction"
echo "5. Find dispute game (game does NOT need to be resolved/finalized)"
echo "6. Get prove parameters (dispute game index & output root proof)"
echo "7. Generate withdrawal Merkle proof (saves to env file)"
echo "8. Prove withdrawal on L1 (auto-runs steps 4-7 if needed)"
echo "9. Resolve and finalize dispute game (required before step 10)"
echo "10. Check withdrawal status (proven / proof maturity / ready to finalize)"
echo "11. Finalize withdrawal on L1 (finalizeWithdrawalTransaction)"
echo "12. Check L1 balances (wallet / token / bridge)"
echo ""
echo "quit - Exit"
echo ""
}
# Parse command line ACTION (if provided)
if [ -n "$ACTION" ]; then
case "$ACTION" in
0) step_0_set_qnt_token_l2 ;;
1) step_1_check_l2_balance ;;
2) step_2_approve_l2_bridge ;;
3) step_3_withdraw ;;
4) step_4_extract_withdrawal ;;
5) step_5_find_game ;;
6) step_6_get_prove_params ;;
7) step_7_generate_proof ;;
8) step_8_prove ;;
9) step_9_resolve_game ;;
10) step_10_check_status ;;
11) step_11_finalize ;;
12) step_12_check_l1_balances ;;
*) echo "Invalid option: $ACTION"; exit 1 ;;
esac
exit 0
fi
# Interactive mode
while true; do
show_menu
read -p "Select an option: " choice
case $choice in
0) step_0_set_qnt_token_l2 ;;
1) step_1_check_l2_balance ;;
2) step_2_approve_l2_bridge ;;
3) step_3_withdraw ;;
4) step_4_extract_withdrawal ;;
5) step_5_find_game ;;
6) step_6_get_prove_params ;;
7) step_7_generate_proof ;;
8) step_8_prove ;;
9) step_9_resolve_game ;;
10) step_10_check_status ;;
11) step_11_finalize ;;
12) step_12_check_l1_balances ;;
quit) exit 0 ;;
*) log_error "Invalid option" ;;
esac
echo ""
done3. Check Rollup Token Balance
Check Rollup token balance for native coin and ERC20 token.
Command:
bash withdraw-finalize.sh 14. Approve Rollup Standard Bridge (ERC20 Only)
Grant the Rollup Standard Bridge permission to spend your ERC20 tokens.
Command:
bash withdraw-finalize.sh 25. Initiate Withdrawal
Start the withdrawal process by sending tokens from Rollup to L1.
Command:
bash withdraw-finalize.sh 3Auto-filled parameters: ROLLUP_WITHDRAW_TX_HASH, ROLLUP_BLOCK_NUMBER
6. Extract Withdrawal Parameters
Extract withdrawal parameters from the Rollup transaction receipt.
Command:
bash withdraw-finalize.sh 4Auto-filled parameters: WITHDRAWAL_NONCE, WITHDRAWAL_SENDER, WITHDRAWAL_TARGET, WITHDRAWAL_VALUE, WITHDRAWAL_GAS_LIMIT, WITHDRAWAL_DATA, WITHDRAWAL_DESTINATION_CHAIN_ID, WITHDRAWAL_IS_NATIVE_TO_ERC20, WITHDRAWAL_HASH, PROOF_SUBMITTER, WITHDRAWAL_BLOCK_NUMBER
7. Find Dispute Game
Find the dispute game that includes your withdrawal block.
Command:
bash withdraw-finalize.sh 5Auto-filled parameters: DISPUTE_GAME_INDEX
** Important:**
- If no game found: You will see an error like
[ERROR] No game found that includes withdrawal block 1900. Wait for the proposer to create a new dispute game (the game's Rollup block number must be >= your withdrawal block number), then run this step again. - If game found: You will see
[INFO] Saved DISPUTE_GAME_INDEX=X to withdraw-config.env. - You CANNOT proceed to the next steps until this step succeeds and
DISPUTE_GAME_INDEXis set.
8. Get Prove Parameters
Get output root proof parameters from the dispute game.
Command:
bash withdraw-finalize.sh 6Auto-filled parameters: OUTPUT_ROOT_PROOF_VERSION, OUTPUT_ROOT_PROOF_STATE_ROOT, OUTPUT_ROOT_PROOF_ROLLUP_BLOCK_HASH, OUTPUT_ROOT_PROOF_MESSAGE_PASSER_STORAGE_ROOT, GAME_ROLLUP_BLOCK_NUMBER
9. Generate Withdrawal Proof
Generate the Merkle proof for your withdrawal.
Command:
bash withdraw-finalize.sh 7Auto-filled parameters: WITHDRAWAL_PROOF, WITHDRAWAL_STORAGE_SLOT
10. Prove Withdrawal on L1
Submit the withdrawal proof to OptimismPortal on L1.
Command:
bash withdraw-finalize.sh 811. Resolve and Finalize Dispute Game
Resolve and finalize the dispute game. Anyone can call this function. This step requires running the same command multiple times with waiting periods:
- First run: Calls
resolveClaim()to resolve the game (afterMAX_CLOCK_DURATIONexpires) - Wait: X seconds for
DISPUTE_GAME_FINALITY_DELAY_SECONDSto pass - Second run: Calls
resolve()to finalize the game
Command:
bash withdraw-finalize.sh 9Important: You must run this step multiple times - once to resolve, wait, then again to finalize.
If MAX_CLOCK_DURATION has not expired yet:
[INFO] Required (MAX_CLOCK_DURATION): X seconds
[WARN] Clock has NOT expired yet. Need X more seconds.
→ Reason: Resolving games requires MAX_CLOCK_DURATION (X seconds) to expire
→ This is the time limit for challenges before a game can be resolved
Please wait X seconds and then re-run this step.
If game is resolved but finality delay has not elapsed yet:
[SUCCESS] Game resolved: DEFENDER_WINS ✓
[INFO] DISPUTE_GAME_FINALITY_DELAY_SECONDS: X seconds
Resolved at: X
Current time: X
Time elapsed: X seconds
[WARN] Finalization delay not yet elapsed
→ Reason: Finalizing games requires DISPUTE_GAME_FINALITY_DELAY_SECONDS (X seconds) after resolution
→ This delay ensures the game outcome is settled before it can be used for withdrawals
Time remaining: X seconds
Please wait X seconds and then re-run this step.
Wait the required time, then run this step again until both resolution and finalization are complete.
12. Check Withdrawal Status
Check if the withdrawal proof has matured and is ready for finalization. This command shows how much time remaining before you can run step 13 (finalization).
Command:
bash withdraw-finalize.sh 10If proof maturity delay has not elapsed yet:
[SUCCESS] Found valid proof: submitter=0x..., timestamp=X
✓ Withdrawal is proven
Proof submitted at timestamp: X
Proof maturity delay: X seconds
→ Reason: Proving withdrawals requires PROOF_MATURITY_DELAY_SECONDS (X seconds) after proof submission
→ This delay ensures the proof has sufficient time to be verified before finalization
Current timestamp: X
Time elapsed since proof: X seconds
⚠ Proof maturity delay not yet elapsed
Time remaining: X seconds (X minutes)
Please wait X seconds and then re-run this step.
If proof maturity delay has elapsed:
[SUCCESS] Found valid proof: submitter=0x..., timestamp=X
✓ Withdrawal is proven
Proof submitted at timestamp: X
Proof maturity delay: X seconds
→ Reason: Proving withdrawals requires PROOF_MATURITY_DELAY_SECONDS (X seconds) after proof submission
→ This delay ensures the proof has sufficient time to be verified before finalization
Current timestamp: X
Time elapsed since proof: X seconds
✓ Proof maturity delay has elapsed
[VERIFY] Calling checkWithdrawal (final verification)
✓ Withdrawal is ready to finalize!
You can now run step 11 to finalize withdrawal
Note: Run this step to check when you can proceed to finalization. Once you see "✓ Withdrawal is ready to finalize!", proceed to step 13.
13. Finalize Withdrawal
Finalize the withdrawal and claim your tokens on L1.
Command:
bash withdraw-finalize.sh 11Result: Your tokens are now on L1!
14. Verify L1 Token Balance
After finalization, verify that your tokens have arrived on L1.
Check your L1 wallet balance:
source withdraw-config.env
cast call "$L1_TOKEN" "balanceOf(address)(uint256)" "$WALLET" --rpc-url "$L1_RPC_URL"Withdrawing Native Coin (Rollup) → ERC20 (L1)
If you want to withdraw native coin from Rollup and receive ERC20 tokens on L1, follow the same steps (3-14) above with the following modified withdraw-config.env file:
Complete Environment Configuration:
# ============================================================================
# WITHDRAWAL CONFIGURATION FILE (withdraw-config.env)
# For Native Coin (Rollup) → ERC20 (L1) Withdrawals
# ============================================================================
# Fill in the parameters below with your specific values.
# Parameters marked as "AUTO-FILLED" will be populated by the scripts.
# ============================================================================
# ----------------------------------------------------------------------------
# WALLET CONFIGURATION
# ----------------------------------------------------------------------------
# Your private key (DO NOT SHARE THIS!)
PRIVATE_KEY="<your-private-key>"
# Your wallet address (derived from private key)
# e.g., cast wallet address --private-key $PRIVATE_KEY
WALLET="<your-wallet-address>"
# ----------------------------------------------------------------------------
# NETWORK CONFIGURATION
# ----------------------------------------------------------------------------
# Rollup RPC endpoint URL
# e.g., https://fusion-rollup.quant.dev/rpc/<your-client-id>
ROLLUP_RPC_URL="<your-rollup-RPC-url>"
# L1 RPC endpoint URL
# e.g., http://localhost:8545 or https://mainnet.infura.io/v3/YOUR-PROJECT-ID
L1_RPC_URL="<your-L1-RPC-url>"
# Rollup chain ID (e.g., 73073 for custom rollup)
ROLLUP_CHAIN_ID="<rollup-chain-id>"
# L1 chain ID (e.g., 1337 for local devnet, 1 for Ethereum mainnet)
L1_CHAIN_IDS="<L1-chain-id>"
# ----------------------------------------------------------------------------
# ROLLUP CONTRACT ADDRESSES (Predeploys - same for all Optimism-based rollups)
# ----------------------------------------------------------------------------
# Rollup Standard Bridge predeploy address
ROLLUP_STANDARD_BRIDGE_ADDRESS="0x4200000000000000000000000000000000000010"
# Rollup Token Whitelist predeploy address
ROLLUP_WHITELIST_ADDRESS="0x4200000000000000000000000000000000000029"
# ----------------------------------------------------------------------------
# L1 CONTRACT ADDRESSES (Deployment-specific - get from your L1 deployment)
# ----------------------------------------------------------------------------
# L1 Standard Bridge proxy address
L1_STANDARD_BRIDGE_PROXY_ADDRESS="<L1-standard-bridge-proxy>"
# Delayed WETH proxy address
DELAYED_WETH_PROXY_ADDRESS="<delayed-weth-proxy>"
# Dispute Game Factory proxy address
DISPUTE_GAME_FACTORY_PROXY_ADDRESS="<dispute-game-factory-proxy>"
# Optimism Portal proxy address
OPTIMISM_PORTAL_PROXY_ADDRESS="<optimism-portal-proxy>"
# Anchor State Registry proxy address
ANCHOR_STATE_REGISTRY_PROXY_ADDRESS="<anchor-state-registry-proxy>"
# ETH Lockbox address (optional, for balance checks)
L1_ETH_LOCK_BOX_PROXY_ADDRESS="<eth-lockbox-address>"
# ----------------------------------------------------------------------------
# TOKEN ADDRESSES (for Native Rollup → ERC20 L1 withdrawals)
# ----------------------------------------------------------------------------
# L1 ERC20 token address (the ERC20 token you'll receive on L1)
L1_TOKEN="<L1-ERC20-token-address>"
# Rollup token address - use address(0) for native coin on Rollup
ROLLUP_TOKEN="0x0000000000000000000000000000000000000000"
# L1 QNT Wrapper token address (if using wrapped QNT)
L1_WRAPPER_TOKEN="<L1-wrapper-token-address>"
# Token decimals (e.g., 18 for most tokens)
TOKEN_DECIMALS="18"
# ----------------------------------------------------------------------------
# WITHDRAWAL PARAMETERS (for Native Rollup → ERC20 L1 withdrawals)
# ----------------------------------------------------------------------------
# Amount to withdraw (in base units, e.g., wei for 18 decimals)
# Example: "1000000000000000000" = 1 native coin with 18 decimals
WITHDRAW_AMOUNT="1000000000000000000"
# Recipient address (leave empty to withdraw to your own wallet)
WITHDRAW_TO=""
# Set to "true" for native coin withdrawals from Rollup
WITHDRAW_ETH="true"
# Minimum gas limit for the withdrawal message (default: 200000)
MIN_GAS_LIMIT="200000"
# Extra data to attach to withdrawal (default: 0x)
EXTRA_DATA="0x"
# Source chain ID (leave empty to use ROLLUP_CHAIN_ID)
SOURCE_CHAIN_ID=""
# Destination chain ID (leave empty to use L1_CHAIN_IDS)
DESTINATION_CHAIN_ID=""
# ============================================================================
# AUTO-FILLED PARAMETERS - Leave these empty, scripts will populate them
# ============================================================================
# --- Filled by Step 3: Withdraw tokens from Rollup to L1 ---
ROLLUP_WITHDRAW_TX_HASH="" # [Step 3] Transaction hash of your Rollup withdrawal
ROLLUP_BLOCK_NUMBER="" # [Step 3] Rollup block number of withdrawal transaction
# --- Filled by Step 4: Extract withdrawal parameters from Rollup transaction ---
WITHDRAWAL_NONCE="" # [Step 4] Withdrawal nonce from RollupToL1MessagePasser
WITHDRAWAL_SENDER="" # [Step 4] Sender address (Rollup CrossDomainMessenger)
WITHDRAWAL_TARGET="" # [Step 4] Target address on L1 (L1 CrossDomainMessenger)
WITHDRAWAL_VALUE="" # [Step 4] Value in wei sent with withdrawal (0 for ERC20)
WITHDRAWAL_GAS_LIMIT="" # [Step 4] Gas limit for withdrawal execution on L1
WITHDRAWAL_DATA="" # [Step 4] Encoded withdrawal data (contains token transfer info)
WITHDRAWAL_DESTINATION_CHAIN_ID="" # [Step 4] Destination chain ID (extracted from MessagePassed event)
WITHDRAWAL_IS_NATIVE_TO_ERC20="" # [Step 4] Flag indicating if native-to-ERC20 withdrawal
WITHDRAWAL_HASH="" # [Step 4] Keccak256 hash of the withdrawal transaction
PROOF_SUBMITTER="" # [Step 4] Address that submitted the proof (defaults to WALLET)
WITHDRAWAL_BLOCK_NUMBER="" # [Step 4] Block number where withdrawal occurred
# --- Filled by Step 5: Find dispute game for withdrawal ---
DISPUTE_GAME_INDEX="" # [Step 5] Index of dispute game that includes your withdrawal
# --- Filled by Step 6: Get prove parameters (output root proof) ---
OUTPUT_ROOT_PROOF_VERSION="" # [Step 6] Output root proof version (always 0 for Bedrock)
OUTPUT_ROOT_PROOF_STATE_ROOT="" # [Step 6] Rollup state root at game's Rollup block
OUTPUT_ROOT_PROOF_ROLLUP_BLOCK_HASH="" # [Step 6] Rollup block hash at game's Rollup block
OUTPUT_ROOT_PROOF_MESSAGE_PASSER_STORAGE_ROOT="" # [Step 6] Storage root of RollupToL1MessagePasser at game's Rollup block
GAME_ROLLUP_BLOCK_NUMBER="" # [Step 6] Rollup block number that dispute game covers
# --- Filled by Step 7: Generate withdrawal Merkle proof ---
WITHDRAWAL_PROOF="" # [Step 7] Merkle proof that withdrawal exists in Rollup state
WITHDRAWAL_STORAGE_SLOT="" # [Step 7] Storage slot where withdrawal is recorded in RollupToL1MessagePasserImportant Notes:
WITHDRAW_ETH="true"- This tells the script you're withdrawing native coin from RollupROLLUP_TOKEN="0x0000000000000000000000000000000000000000"- Use address(0) for native coin on RollupL1_TOKEN="<L1-ERC20-token-address>"- Set this to the actual ERC20 token address on L1- All other steps (3-14) remain the same - Just run the same commands with this modified configuration
Updated about 3 hours ago
