Skip to content

๐Ÿšจ Jovay ForceWithdraw Escape Hatch User Guide โ€‹


๐Ÿ›๏ธ Background: Bankruptcy & Escape Hatch โ€‹

What is Bankruptcy? โ€‹

In the Rollup architecture, the Sequencer/Relayer is responsible for periodically submitting L2 transaction data to L1 and finalizing it. If, due to operator shutdown, an attack, or other reasons, the Rollup contract has not finalized any new Batch for longer than the preset timeout (default: 7 days), the system automatically enters Bankruptcy state:

  • ๐Ÿ”’ The Rollup contract freezes, rejecting new commitBatch / verifyBatch calls
  • โฐ Determination is purely based on on-chain timestamps โ€” no manual trigger required
  • ๐Ÿ“› Rollup.isInactive() returns true

What is the Escape Hatch? โ€‹

The Escape Hatch is the ultimate fund safety guarantee that the Rollup provides to users: even if L2 completely stops running, users can still withdraw their assets through L1 contracts.

  • ๐Ÿ” The proof is bound to msg.sender โ€” only the balance owner can withdraw
  • ๐Ÿ›ก๏ธ Each account can only withdraw once per batchIndex (prevents double-spending)

โš ๏ธ Current Scope: Only ETH escape hatch is supported โ€” ERC20 is not included; only direct withdrawal after bankruptcy is supported โ€” censorship-resistance paths are not included.


๐Ÿ“‹ Overview โ€‹

Prerequisites: You have an ETH balance on Jovay L2, and that balance has been finalized by the Rollup contract.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Jovay RPC  โ”‚  proof  โ”‚ L1ETHBridge  โ”‚ release โ”‚  L1Mailbox   โ”‚  ETH
โ”‚ getProof()  โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚forceWithdraw โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚releaseETH()  โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ ๐Ÿ‘ค User
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ” Step 1: Confirm Rollup has entered Bankruptcy state
๐Ÿ“ฆ Step 2: Obtain Merkle balance proof               โ† jovay_getProof
๐Ÿ“ Step 3: Submit forceWithdraw transaction to L1ETHBridge โ† on-chain verification + ETH release
โœ… Step 4: Verify withdrawal result

๐Ÿ› ๏ธ Prerequisites โ€‹

๐Ÿ“ฆ Tool Installation โ€‹

bash
# Install Foundry (for contract interaction)
curl -L https://foundry.paradigm.xyz | bash
foundryup

# Install jq (for JSON parsing)
brew install jq   # macOS
# apt install jq  # Ubuntu/Debian

๐ŸŒ Register ZAN Node Service โ€‹

This guide uses ZAN RPC node service to access Jovay L2 and Ethereum L1.

๐Ÿ‘‰ Registration Steps:

  1. ๐Ÿ”— Visit ZAN.top
  2. ๐Ÿ“ Sign up and log in
  3. โž• Click "Create API Key" to create a new key
  4. โœ… Obtain your API Key
  5. ๐ŸŽฏ Confirm that Jovay and Ethereum chains are enabled in the service list

๐Ÿ’ก ZAN provides Jovay Testnet/Mainnet and Ethereum Sepolia/Mainnet node services โ€” one key grants access to all networks.

๐Ÿ“œ Contract Addresses โ€‹

๐Ÿงช Testnet (Jovay Testnet + Ethereum Sepolia) โ€‹

ContractAddressNetwork
๐Ÿ—๏ธ Rollup0x79C0bB4EE51D7557E012f2f52db4A4ff85Ca3196Ethereum Sepolia
๐ŸŒ‰ L1ETHBridge0x940eFB877281884699176892B02A3db49f29CDE8Ethereum Sepolia
๐Ÿ“ฌ L1Mailbox0x95fE4eD4327fB138Cd4Bd05a574378942648bA04Ethereum Sepolia

๐ŸŒ Mainnet (Jovay Mainnet + Ethereum Mainnet) โ€‹

ContractAddressNetwork
๐Ÿ—๏ธ Rollup0xe0a28b8918a62edb825055221a1df12c7c81bac1Ethereum Mainnet
๐ŸŒ‰ L1ETHBridge0x922248db4a99bb542539ae7165fb9d7a546fb9f1Ethereum Mainnet
๐Ÿ“ฌ L1Mailbox0x9869a90fdac287519e48aff4cce329907a995162Ethereum Mainnet

๐Ÿ”‘ Environment Variables โ€‹

Choose the configuration for your target network:

๐Ÿงช Testnet Configuration (click to expand)
bash
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐ŸŒ Network Configuration โ€” Testnet
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

# ๐Ÿ“ก L1 Ethereum Sepolia RPC
export L1_RPC="https://api.zan.top/node/v1/eth/sepolia/<YOUR_ZAN_API_KEY>"

# ๐Ÿ“ก Jovay Testnet RPC (for obtaining Merkle balance proof)
export JOVAY_RPC="https://api.zan.top/node/v1/jovay/testnet/<YOUR_ZAN_API_KEY>"

# ๐Ÿ“ Contract addresses
export ROLLUP_ADDR="0x79C0bB4EE51D7557E012f2f52db4A4ff85Ca3196"
export L1_ETH_BRIDGE_ADDR="0x940eFB877281884699176892B02A3db49f29CDE8"

# ๐Ÿ” Your account
export MY_ADDRESS="0x<your_ethereum_address>"
export MY_PRIVATE_KEY="0x<your_private_key>"   # โš ๏ธ Testnet only
๐ŸŒ Mainnet Configuration (click to expand)
bash
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐ŸŒ Network Configuration โ€” Mainnet
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

# ๐Ÿ“ก L1 Ethereum Mainnet RPC
export L1_RPC="https://api.zan.top/node/v1/eth/mainnet/<YOUR_ZAN_API_KEY>"

# ๐Ÿ“ก Jovay Mainnet RPC (for obtaining Merkle balance proof)
export JOVAY_RPC="https://api.zan.top/node/v1/jovay/mainnet/<YOUR_ZAN_API_KEY>"

# ๐Ÿ“ Contract addresses
export ROLLUP_ADDR="0xe0a28b8918a62edb825055221a1df12c7c81bac1"
export L1_ETH_BRIDGE_ADDR="0x922248db4a99bb542539ae7165fb9d7a546fb9f1"

# ๐Ÿ” Your account
export MY_ADDRESS="0x<your_ethereum_address>"
export MY_PRIVATE_KEY="0x<your_private_key>"   # โš ๏ธ Strongly recommend using a hardware wallet!

โš ๏ธ Security Note: For mainnet operations, use a hardware wallet (Ledger/Trezor) or cast send --ledger instead of exposing raw private keys.


๐Ÿ” Step 1: Confirm Rollup Has Entered Bankruptcy State โ€‹

1.1 Query Whether Rollup is Inactive โ€‹

bash
# ๐Ÿ“ž Call Rollup.isInactive() โ€” returns true if the timeout has been exceeded
cast call $ROLLUP_ADDR "isInactive()(bool)" --rpc-url $L1_RPC

โœ… Expected Output:

true

๐Ÿ”ด If it returns false, the Rollup is still operating normally โ€” you do not need to use the escape hatch. Use the normal withdrawal process instead.

1.2 Query Detailed Status (Aggregated Interface) โ€‹

bash
# ๐Ÿ“ž Get all at once: inactive flag + latest finalized batchIndex + stateRoot
cast call $ROLLUP_ADDR "getActivityStatus()(bool,uint256,bytes32)" --rpc-url $L1_RPC

โœ… Expected Output Example:

true                                                              โ† ๐Ÿ“› Timed out
42                                                                โ† ๐Ÿ“ฆ Latest finalized batch index
0x1a2b3c...(stateRoot)                                            โ† ๐ŸŒณ State root of that batch

1.3 Query Timeout Threshold and Timestamp (Optional) โ€‹

bash
# โฐ View timeout threshold (seconds)
echo "โฑ๏ธ  Inactivity Timeout:"
cast call $ROLLUP_ADDR "inactivityTimeout()(uint256)" --rpc-url $L1_RPC

# ๐Ÿ“… Last finalize timestamp
echo "๐Ÿ“… Last Finalized Timestamp:"
cast call $ROLLUP_ADDR "lastFinalizedTimestamp()(uint256)" --rpc-url $L1_RPC

# ๐Ÿงฎ Calculate how long it has been inactive
LAST_TS=$(cast call $ROLLUP_ADDR "lastFinalizedTimestamp()(uint256)" --rpc-url $L1_RPC)
CURRENT_TS=$(date +%s)
ELAPSED=$(( CURRENT_TS - LAST_TS ))
echo "โณ Inactive for: $(( ELAPSED / 86400 )) days $(( (ELAPSED % 86400) / 3600 )) hours"

๐Ÿ“ฆ Step 2: Obtain Merkle Balance Proof โ€‹

ZAN provides full-node RPC access to Jovay, allowing you to directly call jovay_getProof to obtain a balance proof. If you haven't registered with ZAN yet, visit ๐Ÿ‘‰ https://zan.top/service/apikeys to sign up and get an API Key.

โš ๏ธ Important: You Must Specify the Correct Block Height โ€‹

๐Ÿšจ jovay_getProof requires a specific block height, and that height must be the last block of the latest finalized batch in the Rollup contract.

If you don't specify a height (using "latest"), the L2 chain may have continued producing blocks, causing intermediate nodes in the proof to change โ€” resulting in a mismatch with the finalized stateRoot on L1, and the transaction will inevitably revert!

๐Ÿ”Ž How to Get the Correct Block Height โ€‹

Step A: Query lastFinalizedBatchIndex

bash
# ๐Ÿ“ฆ Query the latest finalized batch index from the Rollup
BATCH_INDEX=$(cast call $ROLLUP_ADDR "lastFinalizedBatchIndex()(uint256)" --rpc-url $L1_RPC)
echo "๐Ÿ“ฆ Last Finalized Batch Index: $BATCH_INDEX"

Step B: Get the batch's last block height via Jovay Block Explorer

Network๐Ÿ”— Block Explorer Batches Page
๐Ÿงช Testnethttps://sepolia-explorer.jovay.io/batches
๐ŸŒ Mainnethttps://explorer.jovay.io/batches

Steps:

  1. ๐ŸŒ Open the Batches page for your network
  2. ๐Ÿ” Find the Batch with index $BATCH_INDEX (or append the batch index to the URL)
  3. ๐Ÿ“‹ Check the "End Block" in the Batch details
  4. ๐Ÿงฎ Convert the decimal block height to hexadecimal:
bash
# Suppose the End Block shown in the explorer is 1234567
END_BLOCK=1234567
L2_BLOCK_HEIGHT=$(printf "0x%x" $END_BLOCK)
echo "๐ŸŽฏ Target block height (hex): $L2_BLOCK_HEIGHT"

๐Ÿงช Quick Test (Testnet Example) โ€‹

bash
# ๐Ÿ“ก Use ZAN Testnet endpoint to get proof (replace address and height with your own)
curl -s -X POST https://api.zan.top/node/v1/jovay/testnet/<YOUR_ZAN_API_KEY> \
  -H 'Content-Type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "method": "jovay_getProof",
  "params": [
    "0x495386312416eeb20EB18535f62DbBB6cbE0C054",
    [],
    "0x12D687"
  ],
  "id": 1
}' | jq .

๐ŸŽฏ Get Your Proof Using Environment Variables โ€‹

bash
# ๐Ÿ”‘ Get the Merkle balance proof for your account
# Parameter description:
#   params[0]: Your address
#   params[1]: [] (empty array, means account proof)
#   params[2]: Must be the End Block of lastFinalizedBatch (hexadecimal)
curl -s -X POST $JOVAY_RPC \
  -H 'Content-Type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "method": "jovay_getProof",
  "params": [
    "'$MY_ADDRESS'",
    [],
    "'$L2_BLOCK_HEIGHT'"
  ],
  "id": 1
}' | jq .

โœ… Expected Output Example:

json
{
  "id": 1,
  "jsonrpc": "2.0",
  "result": [
    "0x007805406ab79eee1e87844e69613bd41dbe7a51...(InternalNode 65 bytes)",
    "0x00af2e975bf6523043e5915243bb217264bd5dea...(InternalNode 65 bytes)",
    "...(more InternalNodes)...",
    "0x01178d502402c0e925a749261d0fb1786bca67e6...(LeafNode 113 bytes)"
  ]
}

๐Ÿ“Œ Response Format: result is directly a bytes[] array (not a nested object). Each element is a hex-encoded node:

  • 0x00... prefix = InternalNode (65 bytes): [type=0x00][leftHash:32B][rightHash:32B]
  • 0x01... prefix = LeafNode (113 bytes): [type=0x01][keyHash:32B][value:80B]
  • The last element in the array is your account's leaf node, containing nonce/codeSize/balance/codeHash

๐Ÿ’พ Save Proof to File โ€‹

bash
# ๐Ÿ“ Extract the result array and save to file (result itself is the proof array)
curl -s -X POST $JOVAY_RPC \
  -H 'Content-Type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "method": "jovay_getProof",
  "params": ["'$MY_ADDRESS'", [], "'$L2_BLOCK_HEIGHT'"],
  "id": 1
}' | jq '.result' > proof.json

# ๐Ÿ‘€ View proof contents
echo "๐Ÿ“„ Proof file contents:"
cat proof.json | jq .

# ๐Ÿงฎ Check proof path length
echo "๐Ÿ“Š Proof path node count: $(cat proof.json | jq length)"

Option B: Via State Derivation Node ๐Ÿ”’ โ€‹

If you don't trust any third-party RPC, you can run your own State Derivation (SD) Sequencer to independently derive the complete L2 state from Batch data submitted to L1, without trusting the original Sequencer.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    Read Batch data    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  Local derive  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  L1    โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚  SD Node    โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ Full L2 State โ”‚
โ”‚ (Blob) โ”‚                      โ”‚             โ”‚               โ”‚ jovay_getProofโ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜               โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Once the SD node has finished syncing, you can obtain the balance proof via local RPC:

bash
# Same RPC call as Option A, but the endpoint points to your local SD node
# You must also specify the correct block height (End Block of lastFinalizedBatch)
curl -s -X POST http://localhost:8545 \
  -H 'Content-Type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "method": "jovay_getProof",
  "params": ["'$MY_ADDRESS'", [], "'$L2_BLOCK_HEIGHT'"],
  "id": 1
}' | jq '.result' > proof.json

๐Ÿ”’ Security Advantage: In State Derivation mode, you are completely independent of any third party โ€” you derive the L2 state yourself from L1 data. This is the highest security level for obtaining a proof.

๐Ÿ“– Setup Guide: For full State Derivation node deployment documentation, see: ๐Ÿ‘‰ State Derivation Guide


๐Ÿ“ Step 3: Submit forceWithdraw Transaction โ€‹

3.1 Prepare Proof Parameter โ€‹

bash
# ๐Ÿ“„ Check proof.json content format
cat proof.json
# Expected: a JSON array: ["0x00...", "0x00...", "0x01..."]

3.2 Build and Send Transaction โ€‹

bash
# ๐Ÿš€ Call L1ETHBridge.forceWithdraw(bytes[] calldata proof)
#
# cast supports passing array parameters directly: "[elem1,elem2,...]" format
# Extract from proof.json into cast-compatible format

PROOF_ARRAY=$(cat proof.json | jq -r 'join(",")')

# ๐Ÿ“ค Send transaction (cast will automatically estimateGas, no need to specify gas limit manually)
cast send $L1_ETH_BRIDGE_ADDR \
  "forceWithdraw(bytes[])" \
  "[$(cat proof.json | jq -r 'map("\"" + . + "\"") | join(",")')]" \
  --rpc-url $L1_RPC \
  --private-key $MY_PRIVATE_KEY

โœ… Expected Output:

blockHash            0x...
blockNumber          12345678
contractAddress
...
status               1          โ† ๐ŸŽ‰ Transaction successful!
transactionHash      0xabcdef...

๐Ÿ’ก Gas Consumption Note:

  • A single Poseidon2 hash costs โ‰ˆ 190,000 gas (permute ~182k + input packing overhead ~8-10k)
  • Each Merkle proof layer requires one hash computation โ€” each additional layer adds ~200,000 gas
  • 10-layer proof โ‰ˆ 2.7M gas, 25-layer proof โ‰ˆ 5.69M gas
  • cast send will automatically calculate the appropriate gas limit via eth_estimateGas โ€” no manual specification needed

3.3 Send via Foundry Script (For Advanced Use Cases) โ€‹

If you need more flexible control, you can write a Foundry script:

bash
# ๐Ÿ“ Create script file
cat > ForceWithdraw.s.sol << 'EOF'
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;

import "forge-std/Script.sol";

interface IL1ETHBridge {
    function forceWithdraw(bytes[] calldata proof) external;
}

contract ForceWithdrawScript is Script {
    function run() external {
        // Read from environment variables
        address bridge = vm.envAddress("L1_ETH_BRIDGE_ADDR");
        string memory proofJson = vm.readFile("proof.json");
        
        // Parse proof array
        bytes[] memory proof = abi.decode(
            vm.parseJson(proofJson),
            (bytes[])
        );

        vm.startBroadcast();
        IL1ETHBridge(bridge).forceWithdraw(proof);
        vm.stopBroadcast();
    }
}
EOF

# ๐Ÿš€ Run script
forge script ForceWithdraw.s.sol:ForceWithdrawScript \
  --rpc-url $L1_RPC \
  --private-key $MY_PRIVATE_KEY \
  --broadcast \
  -vvvv

โœ… Step 4: Verify Withdrawal Result โ€‹

4.1 Query Transaction Receipt โ€‹

bash
# ๐Ÿ“œ View transaction details (replace with your tx hash)
TX_HASH="0x<your_transaction_hash>"
cast receipt $TX_HASH --rpc-url $L1_RPC

4.2 Decode Event Logs โ€‹

bash
# ๐Ÿ”Ž View ForceWithdrawETH event
cast receipt $TX_HASH --rpc-url $L1_RPC | grep -A5 "logs"

# ๐Ÿ“Š Or decode the event directly
cast logs \
  --from-block $(cast receipt $TX_HASH --rpc-url $L1_RPC | grep blockNumber | awk '{print $2}') \
  --to-block $(cast receipt $TX_HASH --rpc-url $L1_RPC | grep blockNumber | awk '{print $2}') \
  --address $L1_ETH_BRIDGE_ADDR \
  "ForceWithdrawETH(address indexed user, uint256 amount, uint256 indexed batchIndex)" \
  --rpc-url $L1_RPC

โœ… Expected Output:

ForceWithdrawETH(
  user: 0x<your_address>,
  amount: 2000000000000000000,       โ† ๐ŸŽ‰ 2 ETH withdrawn
  batchIndex: 42
)

4.3 Confirm ETH Balance Change โ€‹

bash
# ๐Ÿ’ฐ Query your L1 ETH balance
echo "๐Ÿ’ฐ Current L1 ETH balance:"
cast balance $MY_ADDRESS --rpc-url $L1_RPC --ether

4.4 Confirm Anti-Replay Flag โ€‹

bash
# ๐Ÿ”’ Query whether it's marked as claimed (should return true)
BATCH_INDEX=$(cast call $ROLLUP_ADDR "lastFinalizedBatchIndex()(uint256)" --rpc-url $L1_RPC)
cast call $L1_ETH_BRIDGE_ADDR \
  "forceWithdrawClaimed(address,uint256)(bool)" \
  $MY_ADDRESS $BATCH_INDEX \
  --rpc-url $L1_RPC

โœ… Expected Output: true (cannot withdraw again under the same batchIndex)


โ“ FAQ โ€‹

๐Ÿ”ด Transaction revert: "Rollup is not inactive" โ€‹

Cause: The Rollup is still operating normally and has not exceeded the inactivity timeout threshold.

Solution: Confirm that isInactive() returns true before proceeding. You can check the remaining time via Step 1.3.

๐Ÿ”ด Transaction revert: "Already claimed" โ€‹

Cause: You have already successfully executed forceWithdraw under the current lastFinalizedBatchIndex.

Solution: Each account can only withdraw once per batchIndex โ€” this is the expected anti-replay protection.

๐Ÿ”ด Transaction revert: "Invalid balance proof" โ€‹

Cause: The submitted Merkle proof failed verification.

Possible reasons:

  • ๐Ÿ“… Most common: The proof was obtained without specifying the correct block height, or "latest" was used, causing intermediate nodes to mismatch the finalized stateRoot
  • ๐Ÿ”‘ The proof was not generated for the msg.sender address (address mismatch)
  • ๐Ÿ”„ Proof data was truncated or corrupted during transmission

Solution:

  1. Check the block explorer (Testnet / Mainnet) to confirm the End Block for the lastFinalizedBatchIndex Batch
  2. Re-obtain the proof using that End Block's hexadecimal height
  3. Ensure you obtain the proof using the same address that will call forceWithdraw

๐Ÿ”ด Transaction revert: "Zero balance" โ€‹

Cause: Your ETH balance in the Jovay L2 state snapshot is 0.

Solution: Confirm that your L2 account actually has an ETH balance in the state corresponding to the finalized batch.

๐Ÿ”ด Transaction revert: "Rollup not set" โ€‹

Cause: The L1ETHBridge contract has not been configured with the Rollup address (contract upgrade initialization incomplete).

Solution: Contact the administrator to confirm the contract upgrade status.

๐ŸŸก High Gas Consumption โ€‹

A single Poseidon2 hash costs โ‰ˆ 190,000 gas (permute ~182k + input packing ~8-10k). Each additional Merkle proof layer adds ~200,000 gas. 10-layer proof โ‰ˆ 2.7M gas, 25-layer proof โ‰ˆ 5.69M gas. cast send will automatically estimate โ€” no need to manually specify gas limit.

๐ŸŸก How much balance can I withdraw? โ€‹

bash
# ๐Ÿ“Š View your balance in the proof (no transaction needed, local parsing)
LEAF=$(cat proof.json | jq -r '.[-1]')
# LeafNode format: type(1B) + keyHash(32B) + value(80B)
# balance is at offset [16, 48) within value
# Can be parsed using cast or Python
echo "๐Ÿ”ข Leaf node (hex): $LEAF"
echo "๐Ÿ’ก Balance is at characters 50-114 of the leaf (positions 66-130 after removing 0x prefix)"

๐Ÿ” Security Tips โ€‹

๐Ÿ›ก๏ธ ItemDescription
Private Key SafetyFor production, use a hardware wallet (Ledger/Trezor) or multisig โ€” never expose raw private keys in the command line
Proof SourceIf you don't trust third-party RPCs, use Option B (State Derivation) to derive state independently
Contract Address VerificationAlways confirm Rollup and L1ETHBridge contract addresses through official channels before operating
Identity BindingforceWithdraw is strictly bound to msg.sender โ€” only the private key holder of the balance-owning address can withdraw
IrreversibleOnce withdrawn successfully, you cannot withdraw again under the same batchIndex โ€” ensure your operation is correct