Overview
SpokePool is the base contract deployed on every supported chain (L2s, sidechains, and L1) enabling users to deposit tokens for cross-chain transfers and relayers to fulfill those deposits. The contract locks deposited tokens, emits events for off-chain relayers, and executes merkle leaves to refund relayers and fulfill slow relays. Contract Location:contracts/SpokePool.sol:35
SpokePool is an abstract contract. Each chain has a specific implementation (e.g.,
Ethereum_SpokePool, Arbitrum_SpokePool, Optimism_SpokePool) that overrides admin verification and bridge-specific logic.Core Responsibilities
- Deposit Handling: Accept user deposits with configurable fees, deadlines, and exclusivity periods
- Relay Fulfillment: Allow relayers to fill deposits by sending tokens to recipients
- Merkle Root Execution: Execute relayer refund and slow relay leaves from validated root bundles
- Cross-Chain Admin: Receive and verify admin messages from HubPool on L1
- UUPS Upgradeable: Upgradeable via UUPS proxy pattern with cross-chain admin control
Architecture
Inheritance
Key Features
- UUPS Upgradeable: Logic contract can be upgraded by cross-domain admin (HubPool)
- EIP-712 Signatures: Supports signed deposit updates for speed-ups
- Multi-call: Batch multiple calls in single transaction
- ERC-7683 Compatible: Implements destination settler interface
- OFT Support: Supports LayerZero OFT token bridging
Storage Variables
Address of the L1 contract (HubPool) that acts as owner of this SpokePool. Unused when SpokePool is deployed on same chain as HubPool.
Address that receives tokens withdrawn from this contract. Typically set to HubPool address for L2 SpokePools.
Counter for generating unique deposit IDs. Increments with each
deposit() call. Note: name is legacy; unsafeDeposit bypasses this counter.Whether fill-related functions are paused. When true,
fillRelay() and related functions revert.Whether deposit-related functions are paused. When true,
deposit() and variants revert (except speedUpDeposit).Array of root bundles relayed from HubPool. Each contains relayer refund and slow relay merkle roots with claimed leaf bitmaps.RootBundle struct:
slowRelayRoot: Merkle root for slow fill leavesrelayerRefundRoot: Merkle root for relayer refund leavesclaimedBitmap: 2D bitmap tracking claimed leaves (256x2^248 max)
Maps relay hash to fill status enum: Unfilled (0), RequestedSlowFill (1), or Filled (2).
Maps (L2 token address, relayer address) to outstanding refund amount. Used when refund transfer fails (e.g., blacklisted relayer).
Maps L2 token address to OFT messenger address for LayerZero bridging.
Immutable Variables
Address of wrapped native token (WETH, WMATIC, etc.). Enables depositing native tokens by wrapping.
Maximum age for quote timestamps. Quote must be within
[currentTime - buffer, currentTime].Maximum fill deadline offset. Fill deadline must be ≤
currentTime + buffer.Key Functions
depositV3
Initiates a cross-chain token transfer. Locks input tokens and emits event for relayers to fill on destination chain.Account credited with the deposit. Can later speed up the deposit by signing updated parameters.
Account receiving funds on destination chain. Can be EOA or contract. If contract and message is non-empty, must implement
handleV3AcrossMessage().Token locked in this contract. If wrappedNativeToken, can pass native token as
msg.value.Token relayer sends to recipient on destination chain. Must be non-zero.
Amount of input token to lock. Sent to relayer as refund (minus system fee) after challenge period.
Amount of output token relayer sends to recipient. Fee is
inputAmount - outputAmount (adjusted for token decimals).Chain ID where recipient receives tokens.
Relayer with exclusive fill rights before exclusivity deadline. Must be non-zero if exclusivity deadline > current time.
HubPool timestamp used to determine system fee. Must be within
[currentTime - depositQuoteTimeBuffer, currentTime].Deadline for filling on destination chain. Must be ≤
currentTime + fillDeadlineBuffer. Fills revert after this timestamp.Determines exclusivity deadline via three modes:
- 0: No exclusivity (deadline = 0)
- ≤ MAX_EXCLUSIVITY_PERIOD_SECONDS (31,536,000): Offset from block.timestamp (subject to reorg risk)
- > MAX_EXCLUSIVITY_PERIOD_SECONDS: Absolute timestamp
Data forwarded to recipient if contract. If non-empty, recipient must implement
handleV3AcrossMessage(address tokenSent, uint256 amount, bytes memory message).- Deposits not paused
outputToken != 0x0quoteTimestampwithin buffer from current timefillDeadline ≤ currentTime + fillDeadlineBuffer- If
msg.value > 0, must equalinputAmountandinputTokenmust bewrappedNativeToken - If exclusivity deadline set,
exclusiveRelayermust be non-zero
FundsDeposited(inputToken, outputToken, inputAmount, outputAmount, destinationChainId, depositId, quoteTimestamp, fillDeadline, exclusivityDeadline, depositor, recipient, exclusiveRelayer, message)
Example:
fillRelay
Fulfills a deposit by sending output tokens to the recipient. Relayer fronts capital and is refunded later via merkle proof.Struct containing all deposit parameters. Must exactly match the
FundsDeposited event from origin chain.V3RelayData fields:depositor: Original depositor (bytes32)recipient: Token recipient (bytes32)exclusiveRelayer: Address with exclusive fill rights (bytes32)inputToken: Token deposited on origin (bytes32)outputToken: Token to send on destination (bytes32)inputAmount: Amount depositedoutputAmount: Amount to send to recipientoriginChainId: Origin chain IDdepositId: Unique deposit identifierfillDeadline: Latest fill timestampexclusivityDeadline: Exclusive relayer deadlinemessage: Data to forward to recipient
Chain where relayer wants refund. Can be any supported chain, not just origin.
Address to receive refund on repayment chain.
- Fills not paused
- Current time ≤ fill deadline
- If before exclusivity deadline, caller must be exclusive relayer
- Relay not already filled or slow-fill requested
- This chain’s ID matches
relayData.destinationChainId - Relayer has sufficient token balance
- Transfers
outputAmountofoutputTokento recipient - If recipient is contract and message non-empty, calls
handleV3AcrossMessage() - Unwraps WETH to ETH for EOA recipients if output token is wrapped native
- Marks relay as filled
- If refund transfer fails, credits
relayerRefundmapping
FilledRelay(inputToken, outputToken, inputAmount, outputAmount, repaymentChainId, originChainId, depositId, fillDeadline, exclusivityDeadline, exclusiveRelayer, relayer, depositor, recipient, messageHash, relayExecutionInfo)
executeRelayerRefundLeaf
Executes a relayer refund leaf from a validated root bundle, sending tokens to relayers who filled deposits.Index of root bundle in
rootBundles array containing this leaf.Leaf data for refunding relayers.RelayerRefundLeaf struct:
amountToReturn: Amount to send back to HubPool (if pool rebalance was negative)chainId: Must match this chain’s IDrefundAmounts: Array of refund amounts per relayerleafId: Leaf identifier for bitmapl2TokenAddress: Token to distributerefundAddresses: Relayer addresses to refund (parallel to refundAmounts)
Merkle inclusion proof for this leaf.
chainIdmatches this chain- Valid merkle proof against
rootBundle.relayerRefundRoot - Leaf not already claimed
- Marks leaf as claimed in bitmap
- Sends
amountToReturntowithdrawalRecipient(HubPool) - Distributes refunds to relayers
- If any refund transfer fails, credits
relayerRefundmapping
ExecutedRelayerRefundRoot(amountToReturn, chainId, refundAmounts, rootBundleId, leafId, l2TokenAddress, refundAddresses, deferredRefunds, caller)
executeSlowRelayLeaf
Executes a slow fill leaf from a validated root bundle, sending tokens from SpokePool reserves directly to the recipient.Slow fill parameters.V3SlowFill struct:
relayData: Original deposit data (V3RelayData)chainId: Must match this chainupdatedOutputAmount: Amount to send (can differ from relayData.outputAmount to adjust fee)
Index of root bundle containing this slow fill.
Merkle inclusion proof.
- Valid merkle proof against
rootBundle.slowRelayRoot - Relay not already filled
- Sufficient SpokePool balance
- Marks relay as filled (same as fast fill)
- Sends
updatedOutputAmountto recipient from SpokePool reserves - No relayer credited (repaymentChainId = 0, relayer = bytes32(0))
FilledRelay(..., fillType: SlowFill)
Slow fills are only possible when input and output tokens are “equivalent” (map to same L1 token via pool rebalance routes). They serve as a fallback when no relayer fills before the deadline.
Cross-Chain Admin Verification
Each SpokePool implementation overrides_requireAdminSender() to verify cross-chain messages:
UUPS Upgradeable Pattern
SpokePool uses OpenZeppelin’s UUPS (Universal Upgradeable Proxy Standard) pattern:- Proxy Contract: Holds all state and delegates calls to logic contract
- Logic Contract: Contains implementation (this contract)
- Upgrade Authorization: Only cross-domain admin (HubPool) can upgrade
- HubPool calls
relaySpokePoolAdminFunction()with upgrade calldata - Message relayed via chain adapter to SpokePool
- SpokePool verifies sender via
_requireAdminSender() - Proxy upgrades to new logic contract
Events
Emitted when user deposits tokens for cross-chain transfer.
Emitted when relayer fills a deposit (or slow fill executed).relayExecutionInfo.fillType:
FastFill (0): Normal relayer fillReplacedSlowFill (1): Fast fill that replaced slow fill request (creates excess funds)SlowFill (2): Executed via executeSlowRelayLeaf
Emitted when slow fill is requested for a deposit.
Emitted when relayer refund leaf is executed.
Emitted when HubPool relays new root bundle.
Admin Functions
pauseDeposits
pauseDeposits
Pause/unpause deposit functions. Affects
deposit() but not speedUpDeposit().pauseFills
pauseFills
Pause/unpause fill functions. Affects
fillRelay() and variants.setCrossDomainAdmin
setCrossDomainAdmin
Change cross-domain admin address (HubPool).
setWithdrawalRecipient
setWithdrawalRecipient
Change withdrawal recipient (where returned funds go).
relayRootBundle
relayRootBundle
Store new root bundle from HubPool. Only callable by admin.
emergencyDeleteRootBundle
emergencyDeleteRootBundle
Delete bad root bundle in emergency.
setOftMessenger
setOftMessenger
Configure OFT messenger for LayerZero token bridging.
Security Considerations
Admin Verification: Each chain implementation must correctly verify cross-chain admin messages to prevent unauthorized upgrades or config changes.
- Reentrancy Protection: All state-changing functions use
nonReentrantmodifier - Fill Status Tracking: Prevents double-fills via
fillStatusesmapping - Deadline Validation: Enforces quote timestamp and fill deadline buffers
- Native Token Handling: Safely wraps/unwraps native tokens with balance checks
- Message Recipient: Validates contract recipients implement
handleV3AcrossMessage() - Merkle Proofs: All root bundle executions verify cryptographic proofs
- Claimed Bitmaps: 2D bitmap prevents leaf re-execution
Deposit Variants
deposit()
Core deposit function using bytes32 types for cross-chain addresses.
depositV3()
Legacy-compatible version using
address types instead of bytes32.depositNow()
Sets
quoteTimestamp to current time and fill deadline as offset. For multisigs with uncertain mine time.unsafeDeposit()
Allows custom nonce instead of global counter. Risky - can create duplicate deposit IDs.
Fill Variants
fillRelay()
Standard fill using original deposit parameters.
fillRelayWithUpdatedDeposit()
Fill using depositor-signed updated parameters (output amount, recipient, message).
fillV3Relay()
Legacy-compatible version using address types.
fill()
ERC-7683 standard fill function using order ID.
Related Contracts
- HubPool - L1 contract that manages liquidity and validates root bundles
- Chain-Specific Implementations:
Ethereum_SpokePool.solArbitrum_SpokePool.solOptimism_SpokePool.solPolygon_SpokePool.sol- And many more…