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
foundryup

Verify installation:

cast --version

1.2 Install jq (JSON processor)

Ubuntu/Debian:

sudo apt-get install jq

macOS:

brew install jq

Verify:

jq --version

1.3 Install bc (arbitrary precision calculator)

Ubuntu/Debian:

sudo apt-get install bc

macOS:

brew install bc

Verify:

bc --version

1.4 Install perl (usually pre-installed)

Ubuntu/Debian:

sudo apt-get install perl

macOS: Usually pre-installed, but if needed:

brew install perl

Verify:

perl --version

1.5 Install python3

Ubuntu/Debian:

sudo apt-get install python3

macOS:

brew install python3

Verify:

python3 --version

2. 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 RollupToL1MessagePasser

Now 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 ContractBlockchainAddress
L1 Standard Bridge ProxyEthereum Sepolia testnet0xca7b5227a6983a1d5c5841df2e8edff12bf59deb
L1 Standard Bridge ProxyPolygon Amoy testnet0x4603d005f60d4f6a4ed54279274767d5f47d3b63
L1 Standard Bridge ProxyQuant Besu Private Network0xe32ca85bee804c26ece4106c6bde609cc92a5e94
Delayed WETH ProxyEthereum Sepolia testnet0x9c98ac07c230b226d99a2833f0ab32e7ffa31174
Delayed WETH ProxyPolygon Amoy testnet0x60ac2b841f2cfa9d1ea8359c1448873ed10db30d
Delayed WETH ProxyQuant Besu Private Network0x4603316187785caede30c8b2cfa0f4adba8c7bee
Dispute Game Factory ProxyEthereum Sepolia testnet0x3dd3c48b5cd4dd19c2fd9ccb109e51b31dc633b7
Dispute Game Factory ProxyPolygon Amoy testnet0xdfb41346ca3f977890c42c92d782455498fc46fe
Dispute Game Factory ProxyQuant Besu Private Network0xb6247b5f8ddcf23113d057aafa2b53ccd6d3c497
Optimism Portal ProxyEthereum Sepolia testnet0x57416c27ffb863957b05da9ce8ecd9db6e3a9682
Optimism Portal ProxyPolygon Amoy testnet0x9a953d672f813cbf99fafc6d928dc9c0224f9b84
Optimism Portal ProxyQuant Besu Private Network0x0ac541fa9273e9a31a789f94112623277c6e83b0
Anchor State Registry ProxyEthereum Sepolia testnet0xf1e5c62a2462dd68ae324153b0aced6ba1248555
Anchor State Registry ProxyPolygon Amoy testnet0x16e5bec945f013b66d530bd1974748207ea88687
Anchor State Registry ProxyQuant Besu Private Network0x338c8460564a7fe56e7b6a3f1540d47707f70513
ETH LockboxEthereum Sepolia testnet0x47f9ecb269f47d0410ec4f3e227f4d191b560b2e
ETH LockboxPolygon Amoy testnet0xa3f13e6f7b090aa37411e02d399d9ed18612b9a5
ETH LockboxQuant Besu Private Network0xbbbb8a48532f726ff6a077b5926f3c3156117cca

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 ""
done

3. Check Rollup Token Balance

Check Rollup token balance for native coin and ERC20 token.

Command:

bash withdraw-finalize.sh 1

4. Approve Rollup Standard Bridge (ERC20 Only)

Grant the Rollup Standard Bridge permission to spend your ERC20 tokens.

Command:

bash withdraw-finalize.sh 2

5. Initiate Withdrawal

Start the withdrawal process by sending tokens from Rollup to L1.

Command:

bash withdraw-finalize.sh 3

Auto-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 4

Auto-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 5

Auto-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_INDEX is set.

8. Get Prove Parameters

Get output root proof parameters from the dispute game.

Command:

bash withdraw-finalize.sh 6

Auto-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 7

Auto-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 8

11. 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:

  1. First run: Calls resolveClaim() to resolve the game (after MAX_CLOCK_DURATION expires)
  2. Wait: X seconds for DISPUTE_GAME_FINALITY_DELAY_SECONDS to pass
  3. Second run: Calls resolve() to finalize the game

Command:

bash withdraw-finalize.sh 9

Important: 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 10

If 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 11

Result: 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 RollupToL1MessagePasser

Important Notes:

  1. WITHDRAW_ETH="true" - This tells the script you're withdrawing native coin from Rollup
  2. ROLLUP_TOKEN="0x0000000000000000000000000000000000000000" - Use address(0) for native coin on Rollup
  3. L1_TOKEN="<L1-ERC20-token-address>" - Set this to the actual ERC20 token address on L1
  4. All other steps (3-14) remain the same - Just run the same commands with this modified configuration