Skip to main content

Overview

The Across Bridge Hooks enable automatic cross-chain token bridging when intents are fulfilled. They integrate with Across Protocol to bridge tokens from the source chain to a destination chain, enabling seamless cross-chain fiat on-ramps. Available Versions:
  • AcrossBridgeHook - V1 implementation (works with Orchestrator)
  • AcrossBridgeHookV2 - V2 implementation (works with OrchestratorV2)
Recommended Use Case: Stablecoin-to-stablecoin bridging only (e.g., USDC on Base → USDC on Arbitrum)

Key Features

Cross-Chain Compatibility

Supports both EVM and non-EVM chains:
  • EVM chains: Addresses are left-padded to bytes32
  • Non-EVM chains (Solana): Native 32-byte addresses used directly

Graceful Fallback

If bridging fails, funds are transferred to the recipient on the source chain instead of reverting. This ensures users always receive funds after making off-chain payments.

Two-Phase Data Model

Signal Time (Commitment):
  • Destination chain ID
  • Output token address
  • Recipient address
  • Minimum output amount
Fulfill Time (JIT Data):
  • Actual output amount
  • Fill deadline
  • Exclusive relayer (from Across API)
  • Exclusivity period

Important Limitations

Not Recommended for Volatile AssetsThe Across Bridge Hook is designed for stablecoin-to-stablecoin routes only. Using it with volatile assets (ETH, WBTC, etc.) can cause failures:
  1. minOutputAmount is committed at signal time
  2. Actual outputAmount is provided at fulfill time
  3. If asset price drops between signal and fulfill, the minimum check fails
  4. User has already made off-chain fiat payment and cannot recover funds if fulfill reverts
For stablecoins, price remains stable and checks reliably pass.

Architecture

Contract References

V1:
  • Location: contracts/hooks/AcrossBridgeHook.sol
  • Interface: IPostIntentHook
  • Orchestrator: Single orchestrator address set at deployment
V2:
  • Location: contracts/hooks/AcrossBridgeHookV2.sol
  • Interface: IPostIntentHookV2
  • Orchestrator: Uses IOrchestratorRegistry for multi-orchestrator support

Dependencies

import { IAcrossSpokePool } from "../external/Interfaces/IAcrossSpokePool.sol";
import { IPostIntentHook } from "../interfaces/IPostIntentHook.sol"; // V1
import { IPostIntentHookV2 } from "../interfaces/IPostIntentHookV2.sol"; // V2

Data Structures

BridgeCommitment

Stored in intent.data (V1) or intent.signalHookData (V2) at signal time:
struct BridgeCommitment {
    uint256 destinationChainId;  // Target chain ID
    bytes32 outputToken;         // Token on destination (bytes32 for Solana support)
    bytes32 recipient;           // Recipient on destination (bytes32 for Solana support)
    uint256 minOutputAmount;     // Minimum acceptable output (slippage protection)
}
Field Details:
  • destinationChainId: Chain ID where tokens will be received
    • Example: 42161 for Arbitrum, 8453 for Base
  • outputToken: Token address on destination chain
    • EVM: Use bytes32(uint256(uint160(tokenAddress))) to convert
    • Solana: Use native 32-byte public key
  • recipient: Where tokens will be sent on destination
    • EVM: Use bytes32(uint256(uint160(recipientAddress))) to convert
    • Solana: Use native 32-byte public key
  • minOutputAmount: Minimum tokens to receive
    • Set to ~99.5% of expected output for stablecoins
    • Protects against unfavorable price movement

AcrossFulfillData

Provided in _fulfillIntentData (V1) or _fulfillHookData (V2) at fulfill time:
// V1
struct AcrossFulfillData {
    bytes32 intentHash;              // Hash of intent being fulfilled
    uint256 outputAmount;            // Actual output amount
    uint32 fillDeadlineOffset;       // Seconds until fill deadline
    bytes32 exclusiveRelayer;        // Exclusive relayer from Across API
    uint32 exclusivityParameter;     // Exclusivity duration in seconds
}

// V2 (intentHash in context)
struct AcrossFulfillData {
    uint256 outputAmount;            // Actual output amount
    uint32 fillDeadlineOffset;       // Seconds until fill deadline
    bytes32 exclusiveRelayer;        // Exclusive relayer from Across API
    uint32 exclusivityParameter;     // Exclusivity duration in seconds
}
Field Details:
  • intentHash: (V1 only) Intent identifier for event correlation
  • outputAmount: How many tokens recipient receives on destination
    • Must be ≥ minOutputAmount or hook falls back to local transfer
  • fillDeadlineOffset: Seconds from current block until fill expires
    • Typical range: 1800 (30 min) to 21600 (6 hours)
    • Longer for slower routes, shorter for fast routes
  • exclusiveRelayer: Relayer with priority access (from Across suggested-fees API)
    • Ensures faster fulfillment by known relayers
  • exclusivityParameter: How long exclusive relayer has priority (seconds)
    • Prevents fill competition during exclusivity window

Usage

Deployment

V1:
AcrossBridgeHook hook = new AcrossBridgeHook(
    usdcAddress,           // Input token (e.g., USDC on Base)
    orchestratorAddress,   // Orchestrator that will call this hook
    spokePoolAddress       // Across SpokePool on this chain
);
V2:
AcrossBridgeHookV2 hook = new AcrossBridgeHookV2(
    usdcAddress,                  // Input token (e.g., USDC on Base)
    orchestratorRegistryAddress,  // Registry of authorized orchestrators
    spokePoolAddress              // Across SpokePool on this chain
);

Signal Intent with Bridge Hook

V1 Example:
// Prepare bridge commitment
BridgeCommitment memory commitment = BridgeCommitment({
    destinationChainId: 42161,  // Arbitrum
    outputToken: bytes32(uint256(uint160(arbitrumUSDC))),
    recipient: bytes32(uint256(uint160(recipientAddress))),
    minOutputAmount: 995e6  // 99.5% of 1000 USDC (allows 0.5% slippage)
});

// Signal intent with hook
Intent memory intent = Intent({
    owner: msg.sender,
    escrow: escrowAddress,
    depositId: depositId,
    amount: 1000e6,
    to: recipientAddress,
    timestamp: block.timestamp,
    paymentMethod: keccak256("Venmo"),
    fiatCurrency: keccak256("USD"),
    conversionRate: 1e18,
    payeeId: keccak256("[email protected]"),
    postIntentHook: address(acrossBridgeHook),
    data: abi.encode(commitment)
});

orchestratorV1.signalIntent(intent, referrer, referrerFee);
V2 Example:
// Prepare bridge commitment
BridgeCommitment memory commitment = BridgeCommitment({
    destinationChainId: 42161,
    outputToken: bytes32(uint256(uint160(arbitrumUSDC))),
    recipient: bytes32(uint256(uint160(recipientAddress))),
    minOutputAmount: 995e6
});

// Signal intent with hook
SignalIntentParams memory params = SignalIntentParams({
    escrow: escrowAddress,
    depositId: depositId,
    amount: 1000e6,
    to: recipientAddress,
    paymentMethod: keccak256("Venmo"),
    fiatCurrency: keccak256("USD"),
    conversionRate: 1e18,
    payeeId: keccak256("[email protected]"),
    referrer: referrer,
    referrerFee: referrerFee,
    postIntentHook: address(acrossBridgeHookV2),
    signalHookData: abi.encode(commitment),
    preIntentHookData: ""
});

orchestratorV2.signalIntent(params);

Fulfill Intent with Bridge

Get Across API Data: Before fulfilling, query Across suggested-fees API:
// Example: Fetch Across suggested fees
const response = await fetch(
  `https://app.across.to/api/suggested-fees?` +
  `inputToken=${baseUSDC}&` +
  `outputToken=${arbitrumUSDC}&` +
  `originChainId=8453&` +
  `destinationChainId=42161&` +
  `amount=1000000000` // 1000 USDC (6 decimals)
);

const { 
  totalRelayFee, 
  relayGasFeePct, 
  exclusiveRelayer, 
  exclusivityDeadline 
} = await response.json();

// Calculate output amount and exclusivity
const outputAmount = 1000000000n - totalRelayFee.total;
const fillDeadlineOffset = 3600; // 1 hour
const exclusivityParameter = exclusivityDeadline - Math.floor(Date.now() / 1000);
V1 Fulfill:
// Prepare fulfill data
AcrossFulfillData memory fulfillData = AcrossFulfillData({
    intentHash: intentHash,
    outputAmount: 998e6,  // From Across API
    fillDeadlineOffset: 3600,
    exclusiveRelayer: bytes32(uint256(uint160(relayerAddress))),  // From Across API
    exclusivityParameter: 60  // From Across API
});

orchestratorV1.fulfillIntent(
    intent,
    abi.encode(fulfillData),
    proof
);
V2 Fulfill:
// Prepare fulfill data (no intentHash needed)
AcrossFulfillData memory fulfillData = AcrossFulfillData({
    outputAmount: 998e6,
    fillDeadlineOffset: 3600,
    exclusiveRelayer: bytes32(uint256(uint160(relayerAddress))),
    exclusivityParameter: 60
});

FulfillIntentParams memory params = FulfillIntentParams({
    intentHash: intentHash,
    proof: proof,
    proofPos: 0,
    fulfillHookData: abi.encode(fulfillData)
});

orchestratorV2.fulfillIntent(params);

Execution Flow

Fallback Behavior

The hook implements graceful degradation to protect users:

Fallback Triggers

  1. OUTPUT_BELOW_MINIMUM: outputAmount < minOutputAmount
    • Price moved unfavorably between signal and fulfill
    • Common with volatile assets (reason not to use hook for non-stablecoins)
  2. BRIDGE_CALL_FAILED: spokePool.depositNow() reverted
    • SpokePool paused
    • Route not supported
    • Across liquidity issues

Fallback Action

When fallback is triggered:
  1. Tokens are transferred to intent.to on the source chain
  2. FallbackTransfer event is emitted with reason code
  3. Transaction succeeds (does not revert)
Rationale: User has already made off-chain fiat payment. Reverting would lock their funds with no way to recover. Fallback ensures they receive tokens even if bridge fails.

Example Fallback Event

event FallbackTransfer(
    bytes32 indexed intentHash,
    address indexed recipient,
    uint256 amount,
    FallbackReason reason  // OUTPUT_BELOW_MINIMUM or BRIDGE_CALL_FAILED
);

Events

AcrossBridgeInitiated

Emitted when bridge deposit successfully initiated:
event AcrossBridgeInitiated(
    bytes32 indexed intentHash,
    uint256 destinationChainId,
    bytes32 outputToken,
    bytes32 recipient,
    uint256 inputAmount,
    uint256 outputAmount,
    uint32 fillDeadlineOffset,
    bytes32 exclusiveRelayer,
    uint32 exclusivityParameter
);

FallbackTransfer

Emitted when bridge cannot be initiated:
event FallbackTransfer(
    bytes32 indexed intentHash,
    address indexed recipient,
    uint256 amount,
    FallbackReason reason
);

Admin Functions

Both V1 and V2 include rescue functions for recovering stuck funds:

Rescue ERC20

function rescueERC20(
    address _token,
    address _to,
    uint256 _amount
) external onlyOwner;

Rescue Native

function rescueNative(
    address payable _to,
    uint256 _amount
) external onlyOwner;

Security Considerations

Authorization

V1: Only the specific orchestrator set at deployment can call execute() V2: Any orchestrator registered in IOrchestratorRegistry can call execute()

Token Handling

  1. Exact Pulls: Hook pulls exact approved amount from orchestrator
  2. Approval Hygiene: Approvals to SpokePool are reset to 0 after operations
  3. No Stranded Funds: Tokens are either bridged or sent to recipient (never stuck)

Price Movement

For stablecoins, set minOutputAmount to ~99.5% of expected:
  • Allows minor bridge fee variation
  • Prevents DoS from tiny price movements
  • Still protects against significant slippage
For volatile assets, this model is unsafe:
  • Price can drop >0.5% in minutes
  • minOutputAmount check will fail
  • User loses access to funds after payment

Reentrancy

Orchestr ator calls hook within reentrancy guard. Hook does not need additional protection.

Cross-Chain Address Conversion

EVM Addresses to bytes32

function _toBytes32(address addr) internal pure returns (bytes32) {
    return bytes32(uint256(uint160(addr)));
}

// Usage
bytes32 recipient = bytes32(uint256(uint160(0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb)));

Solana Addresses

Solana public keys are already 32 bytes:
// JavaScript/TypeScript
import { PublicKey } from '@solana/web3.js';

const recipient = new PublicKey('DYw8j...').toBuffer(); // 32 bytes
const recipientBytes32 = '0x' + recipient.toString('hex');

Integration Checklist

  • Deploy hook with correct input token and spoke pool
  • Verify spoke pool supports your destination chain
  • Test bridge route with small amount first
  • Integrate Across suggested-fees API for relayer parameters
  • Set reasonable fillDeadlineOffset (1800-21600 seconds)
  • Set minOutputAmount to ~99.5% of expected output
  • Monitor FallbackTransfer events for failed bridges
  • Implement fallback notification for users
  • Test with both EVM and non-EVM destinations if needed

Build docs developers (and LLMs) love