Bridge Contracts

Moving value between an L1 (Ethereum) and a Panda rollup involves two sides:

SideContractLanguageRole
L2 (Panda)BridgeBalancePython (Panda)Holds every user's L2 balance; credited on deposit, deducted on withdrawal.
L1 (Ethereum)PandaBridgeSolidityCustodies deposited value, verifies withdrawal Merkle proofs, tracks batches.
HubUniversalGatewaySolidityMulti-rollup message hub between L1 and each L2.
RelaySettlementMailboxSolidityPer-settlement-chain relayer endpoint.

The Panda-side BridgeBalance contract is deployable in the playground (Cross-Chain category → Bridge Balance Ledger). The Solidity contracts are EVM infrastructure run by the bridge operators and are documented here for reference — they are not deployable from the playground.

For the surrounding rollup design, see Rollup Architecture.


Deposit and withdrawal flow

Deposit (L1 -> L2):
  PandaBridge.deposit() / depositToken()
    -> UniversalGateway.enqueueInbound
    -> bridge relayer
    -> SettlementMailbox.receiveFromHub
    -> panda-rollup inbox
    -> sequencer calls BridgeBalance.InboundFromHub(...)   # credits the user

Withdrawal (L2 -> L1):
  BridgeBalance.withdraw()                                  # queues a withdrawal
    -> sequencer builds a Merkle tree of queued withdrawals
    -> root posted to PandaBridge.setWithdrawalRoot()
    -> user calls PandaBridge.withdraw() with a Merkle proof

BridgeBalance (L2 ledger)

BridgeBalance is the L2 endpoint the sequencer credits when a deposit arrives, and the queue where withdrawals are recorded for the next Merkle tree.

Security model

  • Only the authorized sequencer can credit deposits — this prevents minting balance from thin air.
  • Nonce ordering (last_deposit_nonce) prevents replaying the same deposit message.
  • An owner freeze switch pauses withdrawals and transfers in an emergency.
  • All amounts are validated strictly positive and balance checks prevent overdraft.

Methods

MethodKindDescription
deploy(sequencer, rollup_id)constructorSets the authorized sequencer (defaults to deployer) and the expected rollup_id (must be positive).
InboundFromHub(hub_payload, inbox_nonce, rollup_id)callProduction entry point. Sequencer-only. Decodes the packed L1 payload and credits the recipient.
credit_deposit(recipient, amount, nonce, token)callExplicit-args deposit path for direct callers and tests. Sequencer-only. Emits Deposited.
withdraw(amount, l1_recipient, token)callDeducts balance and queues an L2→L1 withdrawal. Returns the withdrawal id. Emits WithdrawalQueued.
transfer(to, amount)callMoves L2 balance between users. Emits Transfer.
freeze() / unfreeze()callOwner emergency pause. Emits Frozen / Unfrozen.
get_balance(user)queryNative balance for a user.
get_token_balance(owner, token)queryBalance for a specific token (or "native").
pending_withdrawals()queryThe full withdrawal queue.
stats()queryTotals: deposited, withdrawn, active balances, pending withdrawals, frozen.

Packed payload format

InboundFromHub receives the exact bytes that L1 PandaBridge emits, hex-encoded with a 0x prefix:

LengthMeaningLayout
52 bytesNative depositrecipient(20) + amount(32, big-endian)
72 bytesToken depositrecipient(20) + amount(32) + token(20)
otherNon-deposit passthroughrecorded but not credited (never reverts)

Native balances live under balances[recipient]. Token balances use the key "{recipient}|{token}", so each ERC-20 has its own sub-ledger while the legacy native mapping stays bit-compatible.


PandaBridge (L1, Solidity)

PandaBridge is the L1 custody + settlement contract (pragma solidity 0.8.24, built on OpenZeppelin AccessControl, ReentrancyGuard, Pausable). It handles ETH and whitelisted ERC-20 deposits, Merkle-proof withdrawals, sequencer batch submission, and an optimistic challenge window.

Roles

  • SEQUENCER_ROLE — submits state batches.
  • BRIDGE_ADMIN_ROLE — sets parameters, whitelists tokens, posts withdrawal roots, pauses.

Key external functions

FunctionDescription
deposit(l2Recipient) payableDeposit ETH; routes a 52-byte packed payload to the hub. Emits Deposited.
depositToken(token, l2Recipient, amount)Deposit a whitelisted ERC-20; routes a 72-byte payload. Emits TokenDeposited.
withdraw(l1Recipient, amount, merkleProof)Withdraw ETH against the posted withdrawalRoot. Leaf = keccak256(abi.encodePacked(l1Recipient, amount, msg.sender, address(0))).
withdrawToken(token, l1Recipient, amount, merkleProof)Withdraw an ERC-20; the leaf pins the token address.
submitBatch(batchNumber, preStateRoot, postStateRoot, txData, proof)Sequencer submits a state batch (optionally ZK-verified). Emits BatchSubmitted.
challengeBatch(batchNumber) / finalizeBatch(batchNumber) / rejectBatch(batchNumber)Optimistic challenge lifecycle.
setWithdrawalRoot(bytes32)Admin posts the Merkle root of queued L2 withdrawals.

Withdrawals are intentionally not pausable so users can always exit. The challenge window defaults to ~7 days (50_400 blocks).


UniversalGateway (L1 hub, Solidity)

UniversalGateway is the multi-rollup hub. It stores the latest outbound Merkle root per rollup, verifies and executes outbound messages, and enqueues inbound messages toward each L2.

FunctionDescription
setLatestOutboundRoot(rollupId, root)Gateway admin posts a rollup's outbound root. Emits OutboundRootUpdated.
executeOutboundMessage(rollupId, leaf, merkleProof, target, payload)Verifies the leaf against the root, consumes it (replay-protected), then calls target. Emits UniversalMessageExecuted.
enqueueInbound(rollupId, dest, payload)Inbox-sender role enqueues an L1→L2 message (bounded by MAX_INBOUND_PAYLOAD). Emits L1ToL2Message.
outboundMessageLeaf(rollupId, target, payload)Pure helper: keccak256(abi.encode(rollupId, target, keccak256(payload))).

An optional outbound-target allow-list gates which contracts messages may call.


SettlementMailbox (Solidity)

SettlementMailbox is the per-settlement-chain relayer endpoint. The relayer holds RELAYER_ROLE and bridges messages both directions:

FunctionDescription
receiveFromHub(rollupId, nonce, dest, payload)Native inbound; emits InboundFromHub.
receiveFromHub(rollupId, nonce, dest, token, payload)Token-aware inbound; emits InboundFromHubWithToken only (avoids double-crediting).
emitOutboundToHub(rollupId, target, payload)Builds a leaf matching UniversalGateway.outboundMessageLeaf and emits OutboundToHub.

Try it