Skip to main content

What is Hub-and-Spoke?

Across Protocol uses a hub-and-spoke model where:
  • Hub (HubPool): Central contract on Ethereum L1 that manages liquidity, validates transfers, and owns all spoke contracts
  • Spokes (SpokePools): Lightweight contracts on each supported chain that handle deposits and fills
This architecture centralizes complex logic and liquidity on L1 while keeping L2 contracts simple and gas-efficient.

HubPool: The Central Hub on L1

Core Responsibilities

The HubPool on Ethereum L1 serves as the protocol’s control center:

Liquidity Pool

Accepts deposits from LPs who earn fees for providing capital to fund relayer refunds.

Validation Engine

Validates cross-chain transfers using merkle root bundles with optimistic verification.

Pool Rebalancer

Coordinates token distribution across all SpokePools via chain adapters.

Cross-Chain Owner

Owns and administers all SpokePool contracts across all chains.

Liquidity Management

The HubPool manages liquidity for all supported tokens:
/**
 * @notice Deposit liquidity into this contract to earn LP fees.
 * @notice Caller will receive an LP token representing their share of this pool.
 */
function addLiquidity(address l1Token, uint256 l1TokenAmount) 
    public 
    payable 
    override 
    nonReentrant 
    unpaused 
{
    require(pooledTokens[l1Token].isEnabled, "Token not enabled");
    require(((address(weth) == l1Token) && msg.value == l1TokenAmount) || msg.value == 0, "Bad msg.value");

    uint256 lpTokensToMint = (l1TokenAmount * 1e18) / _exchangeRateCurrent(l1Token);
    pooledTokens[l1Token].liquidReserves += l1TokenAmount;
    ExpandedIERC20(pooledTokens[l1Token].lpToken).mint(msg.sender, lpTokensToMint);

    if (address(weth) == l1Token && msg.value > 0) 
        WETH9Interface(address(l1Token)).deposit{ value: msg.value }();
    else 
        IERC20(l1Token).safeTransferFrom(msg.sender, address(this), l1TokenAmount);

    emit LiquidityAdded(l1Token, l1TokenAmount, lpTokensToMint, msg.sender);
}
Key Concepts:
  • Liquid Reserves: Tokens available in the HubPool for immediate use
  • Utilized Reserves: Tokens sent to SpokePools for relayer refunds
  • LP Fee Rate: Continuous fee distribution using exponential decay (0.0000015e18 per second)
  • Exchange Rate: LP token value increases over time as fees accrue

Root Bundle Validation

The HubPool uses merkle trees to validate cross-chain activity:
/**
 * @notice Publish a new root bundle. Caller stakes a bond that can be slashed
 * if the proposal is invalid.
 */
function proposeRootBundle(
    uint256[] calldata bundleEvaluationBlockNumbers,
    uint8 poolRebalanceLeafCount,
    bytes32 poolRebalanceRoot,
    bytes32 relayerRefundRoot,
    bytes32 slowRelayRoot
) public override nonReentrant noActiveRequests unpaused {
    require(poolRebalanceLeafCount > 0, "Bundle must have at least 1 leaf");

    uint32 challengePeriodEndTimestamp = uint32(getCurrentTime()) + liveness;

    delete rootBundleProposal;

    rootBundleProposal.challengePeriodEndTimestamp = challengePeriodEndTimestamp;
    rootBundleProposal.unclaimedPoolRebalanceLeafCount = poolRebalanceLeafCount;
    rootBundleProposal.poolRebalanceRoot = poolRebalanceRoot;
    rootBundleProposal.relayerRefundRoot = relayerRefundRoot;
    rootBundleProposal.slowRelayRoot = slowRelayRoot;
    rootBundleProposal.proposer = msg.sender;

    bondToken.safeTransferFrom(msg.sender, address(this), bondAmount);

    emit ProposeRootBundle(
        challengePeriodEndTimestamp,
        poolRebalanceLeafCount,
        bundleEvaluationBlockNumbers,
        poolRebalanceRoot,
        relayerRefundRoot,
        slowRelayRoot,
        msg.sender
    );
}
Three Merkle Trees:
  1. Pool Rebalance Root: Instructions for sending tokens from HubPool to SpokePools
  2. Relayer Refund Root: Merkle proofs for relayer refunds on each chain
  3. Slow Relay Root: Merkle proofs for slow fills when no relayer filled the deposit

SpokePools: Lightweight Spokes on L2s

Core Responsibilities

SpokePools on each chain are responsible for:
1

Accept Deposits

Users lock tokens in the SpokePool and emit a FundsDeposited event that relayers monitor.
2

Process Fills

Relayers call fillRelay() to send tokens to recipients and record the fill.
3

Receive Root Bundles

Accept merkle roots from HubPool containing refund and slow fill instructions.
4

Execute Merkle Leaves

Process relayer refunds and slow fills using merkle proofs.

Deposit Flow

/**
 * @notice Request to bridge input token cross chain to a destination chain.
 */
function deposit(
    bytes32 depositor,
    bytes32 recipient,
    bytes32 inputToken,
    bytes32 outputToken,
    uint256 inputAmount,
    uint256 outputAmount,
    uint256 destinationChainId,
    bytes32 exclusiveRelayer,
    uint32 quoteTimestamp,
    uint32 fillDeadline,
    uint32 exclusivityParameter,
    bytes calldata message
) public payable override nonReentrant unpausedDeposits {
    DepositV3Params memory params = DepositV3Params({
        depositor: depositor,
        recipient: recipient,
        inputToken: inputToken,
        outputToken: outputToken,
        inputAmount: inputAmount,
        outputAmount: outputAmount,
        destinationChainId: destinationChainId,
        exclusiveRelayer: exclusiveRelayer,
        depositId: numberOfDeposits++,
        quoteTimestamp: quoteTimestamp,
        fillDeadline: fillDeadline,
        exclusivityParameter: exclusivityParameter,
        message: message
    });
    _depositV3(params);
}
Key Features:
  • Lightweight: Minimal logic to reduce gas costs on L2s
  • Event-Driven: Deposits emit events that off-chain relayers monitor
  • Merkle Proofs: Refunds and slow fills use merkle proofs to validate against roots
  • Pausable: Admin can pause deposits or fills in emergencies

Cross-Chain Ownership

The HubPool on L1 owns all SpokePool contracts across all chains through a cross-chain ownership model:
1

Admin Function Call

HubPool owner calls relaySpokePoolAdminFunction(chainId, functionData) on HubPool.
2

Chain Adapter Delegation

HubPool delegates to the appropriate chain adapter via delegatecall.
3

Cross-Chain Message

Adapter sends message through the chain’s native bridge (Arbitrum Inbox, OP Messenger, etc.).
4

Admin Verification

SpokePool’s _requireAdminSender() verifies the message came from HubPool.
5

Function Execution

SpokePool executes the admin function (upgrade, pause, configure, etc.).
/**
 * @notice Sends message to SpokePool from this contract. Callable only by owner.
 */
function relaySpokePoolAdminFunction(
    uint256 chainId,
    bytes memory functionData
) public override onlyOwner nonReentrant {
    _relaySpokePoolAdminFunction(chainId, functionData);
}

function _relaySpokePoolAdminFunction(uint256 chainId, bytes memory functionData) internal {
    (address adapter, address spokePool) = _getInitializedCrossChainContracts(chainId);
    
    (bool success, ) = adapter.delegatecall(
        abi.encodeWithSignature(
            "relayMessage(address,bytes)",
            spokePool,
            functionData
        )
    );
    require(success, "delegatecall failed");
    
    emit SpokePoolAdminFunctionTriggered(chainId, functionData);
}
Chain-Specific Verification:
  • Arbitrum: Uses address aliasing (AddressAliasHelper.applyL1ToL2Alias())
  • Optimism/Base: Verifies via CrossDomainMessenger.xDomainMessageSender()
  • Polygon: Checks FxChild.processMessageFromRoot()
  • zkSync: Validates via zkSync bridge messaging

UUPS Upgrades

All SpokePools use UUPS (Universal Upgradeable Proxy Standard) for upgrades:
/**
 * @notice Called by {upgradeTo} and {upgradeToAndCall}.
 * @dev Only admin can authorize upgrades.
 */
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}

modifier onlyAdmin() {
    _requireAdminSender();
    _;
}
Upgrade Process:
  1. Deploy new SpokePool implementation
  2. Call relaySpokePoolAdminFunction() on HubPool with upgradeTo(newImplementation)
  3. Message bridges to L2 via chain adapter
  4. SpokePool verifies admin and upgrades to new implementation
  5. Storage and address remain unchanged

Pool Rebalancing

The hub-and-spoke model enables efficient pool rebalancing:
Relayers fill deposits on destination chains using their own capital. The HubPool must periodically send tokens to SpokePools to refund relayers. This process is called “pool rebalancing.”
  1. Data worker monitors all chains and calculates net token flows
  2. Data worker proposes a root bundle with pool rebalance leaves
  3. After challenge period, HubPool executes leaves to send tokens to SpokePools
  4. Relayers claim refunds on their chosen chains using merkle proofs
Net flow calculation means tokens only move when needed. If a chain has excess tokens, they can be sent back to HubPool or redirected to deficit chains.

Benefits of Hub-and-Spoke

Centralized Liquidity

All LP capital pooled on L1 for maximum capital efficiency. No fragmentation across chains.

Simplified L2 Logic

SpokePools are lightweight with minimal logic, reducing gas costs and attack surface.

Unified Governance

Single point of control on L1 for all protocol parameters and upgrades.

Optimistic Verification

Challenge period on L1 ensures security without expensive on-chain verification.

Flexible Scaling

Easy to add new chains by deploying a SpokePool and chain adapter.

Efficient Rebalancing

Net flow calculations minimize unnecessary token transfers.

Architecture

Overview of the complete system architecture

Protocol Flow

Step-by-step guide through the protocol flow

Roles

Learn about different participants in the protocol

Build docs developers (and LLMs) love