Documentation Index
Fetch the complete documentation index at: https://mintlify.com/zkp2p/zkp2p-contracts/llms.txt
Use this file to discover all available pages before exploring further.
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:
minOutputAmount is committed at signal time
- Actual
outputAmount is provided at fulfill time
- If asset price drops between signal and fulfill, the minimum check fails
- 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("user@venmo.com"),
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("user@venmo.com"),
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
-
OUTPUT_BELOW_MINIMUM:
outputAmount < minOutputAmount
- Price moved unfavorably between signal and fulfill
- Common with volatile assets (reason not to use hook for non-stablecoins)
-
BRIDGE_CALL_FAILED:
spokePool.depositNow() reverted
- SpokePool paused
- Route not supported
- Across liquidity issues
Fallback Action
When fallback is triggered:
- Tokens are transferred to
intent.to on the source chain
FallbackTransfer event is emitted with reason code
- 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
- Exact Pulls: Hook pulls exact approved amount from orchestrator
- Approval Hygiene: Approvals to SpokePool are reset to 0 after operations
- 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