Across Protocol + Biconomy MEE Integration Guide
๐ Cross-Chain USDC Supply to AAVE with Gasless Fusion Orchestration
This guide demonstrates how to use Across Protocol together with Biconomy's Modular Execution Environment (MEE) to perform a cross-chain supply of USDC into AAVE, completely gasless using Fusion Orchestration.
๐ฏ Overview
The integration enables:
- ๐ Cross-chain bridging from Optimism to Base using Across Protocol
- ๐ฆ Automated AAVE supply on the destination chain
- โฝ Gasless execution through Fusion Orchestration
- โ๏ธ Single signature UX for the entire flow
- โก Ultra-fast bridging with Across's optimistic design
๐๏ธ Architecture
Key Components
- ๐ Across Protocol: Ultra-fast optimistic bridge with capital-efficient design
- ๐ง Biconomy MEE: Orchestrates the entire transaction flow
- ๐ญ Fusion Mode: Enables gasless execution with external wallets
- ๐ค Companion Account: Temporary smart account for orchestration
Flow Diagram
User EOA (Optimism) ๐ฐ
โ [Sign Trigger] โ๏ธ
Companion Account ๐ค
โ [Bridge via Across] ๐
Base Network โ๏ธ
โ [Supply to AAVE] ๐ฆ
aUSDC โ User EOA (Base) ๐
๐ Implementation Guide
1. Setup and Dependencies
First, create the Across Quote Service file. Copy and paste this entire code block into a new file called across-quote-service.ts
:
// across-quote-service.ts
import type { Address, Hex } from 'viem';
import { getAddress, formatUnits, parseUnits } from 'viem';
/**
* -------------------------------------------------------------
* Across Protocol Quote Service โ typed with viem
* -------------------------------------------------------------
*/
export type SuggestedFeesParameters = {
inputToken: Address;
outputToken: Address;
originChainId: number;
destinationChainId: number;
amount: bigint;
depositor?: Address;
recipient?: Address;
message?: Hex;
relayerAddress?: Address;
referrer?: Address;
};
export type Fee = {
pct: bigint;
total: bigint;
};
export type Limits = {
minDeposit: bigint;
maxDeposit: bigint;
maxDepositInstant: bigint;
maxDepositShortDelay: bigint;
recommendedDepositInstant: bigint;
};
export type SuggestedFeesReturnType = {
totalRelayFee: Fee;
relayerCapitalFee: Fee;
relayerGasFee: Fee;
lpFee: Fee;
timestamp: bigint;
isAmountTooLow: boolean;
quoteBlock: bigint;
spokePoolAddress: Address;
fillDeadline: bigint;
limits: Limits;
};
export type GetSuggestedFeesErrorType = Error;
/**
* Gets suggested fees for Across Protocol bridge transfer
*
* @example
* const fees = await getAcrossSuggestedFees({
* inputToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
* outputToken: '0x4200000000000000000000000000000000000006',
* originChainId: 1,
* destinationChainId: 10,
* amount: parseEther('1')
* })
*/
export async function getAcrossSuggestedFees(
parameters: SuggestedFeesParameters
): Promise<SuggestedFeesReturnType> {
const {
inputToken,
outputToken,
originChainId,
destinationChainId,
amount,
depositor,
recipient,
message,
relayerAddress,
referrer
} = parameters;
const url = new URL('https://app.across.to/api/suggested-fees');
// Validate and format addresses
url.searchParams.append('inputToken', getAddress(inputToken));
url.searchParams.append('outputToken', getAddress(outputToken));
url.searchParams.append('originChainId', originChainId.toString());
url.searchParams.append('destinationChainId', destinationChainId.toString());
url.searchParams.append('amount', amount.toString());
if (depositor) {
url.searchParams.append('depositor', getAddress(depositor));
}
if (recipient) {
url.searchParams.append('recipient', getAddress(recipient));
}
if (message) {
url.searchParams.append('message', message);
}
if (relayerAddress) {
url.searchParams.append('relayerAddress', getAddress(relayerAddress));
}
if (referrer) {
url.searchParams.append('referrer', getAddress(referrer));
}
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json() as {
totalRelayFee: { pct: string; total: string };
relayerCapitalFee: { pct: string; total: string };
relayerGasFee: { pct: string; total: string };
lpFee: { pct: string; total: string };
timestamp: string;
isAmountTooLow: boolean;
quoteBlock: string;
spokePoolAddress: string;
expectedFillTimeSec: string;
fillDeadline: string;
limits: {
minDeposit: string;
maxDeposit: string;
maxDepositInstant: string;
maxDepositShortDelay: string;
recommendedDepositInstant: string;
};
};
// Parse response to bigint types
return {
totalRelayFee: {
pct: BigInt(data.totalRelayFee.pct),
total: BigInt(data.totalRelayFee.total)
},
relayerCapitalFee: {
pct: BigInt(data.relayerCapitalFee.pct),
total: BigInt(data.relayerCapitalFee.total)
},
relayerGasFee: {
pct: BigInt(data.relayerGasFee.pct),
total: BigInt(data.relayerGasFee.total)
},
lpFee: {
pct: BigInt(data.lpFee.pct),
total: BigInt(data.lpFee.total)
},
timestamp: BigInt(data.timestamp),
isAmountTooLow: data.isAmountTooLow,
quoteBlock: BigInt(data.quoteBlock),
spokePoolAddress: getAddress(data.spokePoolAddress),
fillDeadline: BigInt(data.fillDeadline),
limits: {
minDeposit: BigInt(data.limits.minDeposit),
maxDeposit: BigInt(data.limits.maxDeposit),
maxDepositInstant: BigInt(data.limits.maxDepositInstant),
maxDepositShortDelay: BigInt(data.limits.maxDepositShortDelay),
recommendedDepositInstant: BigInt(data.limits.recommendedDepositInstant)
}
};
}
/**
* Calculates total fees from suggested fees response
*
* @example
* const result = calculateAcrossFees({
* fees,
* amount: parseEther('1')
* })
*
* console.log(result.totalFees) // 0.005n (example)
* console.log(result.outputAmount) // 0.995n (example)
*/
export type CalculateAcrossFeesParameters = {
fees: SuggestedFeesReturnType;
amount: bigint;
};
export type CalculateAcrossFeesReturnType = {
totalFees: bigint;
relayerFees: bigint;
lpFees: bigint;
outputAmount: bigint;
};
export function calculateAcrossFees(
parameters: CalculateAcrossFeesParameters
): CalculateAcrossFeesReturnType {
const { fees, amount } = parameters;
// Calculate fees based on percentages (denominated in 1e18)
const PRECISION = 10n ** 18n;
const relayerFees = (amount * fees.totalRelayFee.pct) / PRECISION;
const lpFees = (amount * fees.lpFee.pct) / PRECISION;
const totalFees = relayerFees + lpFees;
const outputAmount = amount - totalFees;
return {
totalFees,
relayerFees,
lpFees,
outputAmount
};
}
/**
* Formats Across fee percentage to human readable format
*
* @example
* formatAcrossFeePercentage(376607094864283n) // "0.0376607094864283"
*/
export function formatAcrossFeePercentage(pct: bigint): string {
return formatUnits(pct, 16); // Across uses 1e18 for 100%, so 1e16 for 1%
}
Now, import the dependencies in your main file:
import {
createMeeClient,
toMultichainNexusAccount,
createChainAddressMap,
runtimeERC20BalanceOf,
greaterThanOrEqualTo,
type Trigger
} from "@biconomy/abstractjs"
import {
getAcrossSuggestedFees,
calculateAcrossFees
} from "./across-quote-service" // ๐ Import the service you just created
import { base, optimism } from "viem/chains"
import { privateKeyToAccount } from "viem/accounts"
import { encodeFunctionData, http, parseAbi, parseUnits, zeroAddress } from "viem"
2. ๐ Configure Token and Protocol Addresses
// ๐ต USDC addresses on different chains
const usdcAddresses = createChainAddressMap([
[base.id, '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'],
[optimism.id, '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85']
])
// ๐ฆ AAVE Pool addresses
const aavePoolAddresses = createChainAddressMap([
[base.id, '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5'],
[optimism.id, '0x794a61358D6845594F94dc1DB02A252b5b4814aD']
])
// ๐ aUSDC (AAVE interest-bearing USDC)
const aUSDCAddresses = createChainAddressMap([
[base.id, '0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB'],
[optimism.id, '0x625E7708f30cA75bfd92586e17077590C60eb4cD']
])
// ๐ Across SpokePool addresses
const acrossSpokePool = createChainAddressMap([
[base.id, '0x09aea4b2242abc8bb4bb78d537a67a245a7bec64'],
[optimism.id, '0x6f26Bf09B1C792e3228e5467807a900A503c0281']
])
3. ๐ Initialize MEE Client and Orchestrator
// Create orchestrator account
const eoa = privateKeyToAccount(PRIVATE_KEY)
const orchestrator = await toMultichainNexusAccount({
chains: [optimism, base],
transports: [http(), http('https://base.llamarpc.com')],
signer: eoa
})
// Initialize MEE client
const meeClient = await createMeeClient({
account: orchestrator,
apiKey: 'your_mee_api_key'
})
4. ๐ Get Across Bridge Quote & Build Transaction
โ ๏ธ Important: Make sure you've created the across-quote-service.ts
file from Step 1 before proceeding!
Unlike other bridges, with Across we build the transaction ourselves:
// Get suggested fees from Across
const fees = await getAcrossSuggestedFees({
amount: inputAmount,
originChainId: optimism.id,
inputToken: usdcAddresses[optimism.id],
destinationChainId: base.id,
outputToken: usdcAddresses[base.id]
})
// Calculate output amount after fees
const { outputAmount } = calculateAcrossFees({
fees,
amount: inputAmount
})
// Define Across SpokePool ABI
const acrossSpokePoolAbi = parseAbi([
'function depositV3(address depositor, address recipient, address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes calldata message) external payable'
])
// Build the deposit transaction
const transactionRequest = {
to: acrossSpokePool[optimism.id],
chainId: optimism.id,
data: encodeFunctionData({
abi: acrossSpokePoolAbi,
functionName: 'depositV3',
args: [
orchestrator.addressOn(optimism.id)!, // depositor
orchestrator.addressOn(base.id)!, // recipient
usdcAddresses[optimism.id], // inputToken
usdcAddresses[base.id], // outputToken
inputAmount, // inputAmount
outputAmount, // outputAmount (after fees)
BigInt(base.id), // destinationChainId
zeroAddress, // exclusiveRelayer
Number(fees.timestamp), // quoteTimestamp
Number(fees.fillDeadline), // fillDeadline
0, // exclusivityDeadline
'0x' // message
]
})
}
5. ๐ ๏ธ Build Orchestration Instructions
Step 1: Define Trigger ๐ฌ
The trigger initiates the orchestration by pulling tokens from the EOA:
const trigger: Trigger = {
chainId: optimism.id,
tokenAddress: usdcAddresses[optimism.id],
amount: inputAmount,
}
Step 2: Approve Across Protocol โ
const approveAcross = await orchestrator.buildComposable({
type: 'approve',
data: {
amount: inputAmount,
chainId: optimism.id,
spender: transactionRequest.to,
tokenAddress: usdcAddresses[optimism.id]
}
})
Step 3: Execute Bridge Transaction ๐
const callAcrossInstruction = await orchestrator.buildComposable({
type: 'rawCalldata',
data: {
to: transactionRequest.to,
calldata: transactionRequest.data,
chainId: transactionRequest.chainId,
}
})
Step 4: Approve AAVE (with runtime balance) ๐
Using runtime balance ensures we approve exactly what arrived:
const approveAAVEInstruction = await orchestrator.buildComposable({
type: 'approve',
data: {
amount: runtimeERC20BalanceOf({
targetAddress: orchestrator.addressOn(base.id)!,
tokenAddress: usdcAddresses[base.id],
constraints: [greaterThanOrEqualTo(1n)]
}),
chainId: base.id,
spender: aavePoolAddresses[base.id],
tokenAddress: usdcAddresses[base.id]
}
})
Step 5: Supply to AAVE ๐ฆ
const aaveSupplyAbi = parseAbi([
'function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)'
])
const callAAVEInstruction = await orchestrator.buildComposable({
type: 'default',
data: {
abi: aaveSupplyAbi,
chainId: base.id,
functionName: 'supply',
to: aavePoolAddresses[base.id],
args: [
usdcAddresses[base.id],
runtimeERC20BalanceOf({
targetAddress: orchestrator.addressOn(base.id)!,
tokenAddress: usdcAddresses[base.id],
constraints: [greaterThanOrEqualTo(1n)]
}),
orchestrator.addressOn(base.id)!,
0
]
}
})
Step 6: Withdraw aUSDC to EOA ๐
const withdrawInstruction = await orchestrator.buildComposable({
type: 'withdrawal',
data: {
amount: runtimeERC20BalanceOf({
targetAddress: orchestrator.addressOn(base.id)!,
tokenAddress: aUSDCAddresses[base.id],
constraints: [greaterThanOrEqualTo(1n)]
}),
chainId: base.id,
tokenAddress: aUSDCAddresses[base.id],
}
})
6. ๐ฏ Execute Fusion Orchestration
const fusionQuote = await meeClient.getFusionQuote({
trigger,
instructions: [
approveAcross,
callAcrossInstruction,
approveAAVEInstruction,
callAAVEInstruction,
withdrawInstruction
],
cleanUps: [{
chainId: base.id,
recipientAddress: eoa.address,
tokenAddress: usdcAddresses[base.id]
}],
feeToken: {
address: usdcAddresses[optimism.id],
chainId: optimism.id
},
lowerBoundTimestamp: nowInSeconds,
upperBoundTimestamp: nowInSeconds + 60
})
const { hash } = await meeClient.executeFusionQuote({ fusionQuote })
console.log(getMeeScanLink(hash))
๐ง Key Concepts
๐ฏ Across Optimistic Design
Across uses an optimistic architecture that:
- โก Lightning fast: Relayers front capital for instant fills
- ๐ฐ Capital efficient: Uses a single liquidity pool across all chains
- ๐ Secure: UMA's optimistic oracle validates all transfers
๐ Runtime Balance Constraints
The runtimeERC20BalanceOf
function ensures instructions use the exact amount that arrives:
- โ Handles bridge fees automatically
- โ Ensures proper sequencing
- โ Avoids failed transactions
๐ Constraints and Execution Order
Instructions execute only when their constraints are met:
- ๐ฆ AAVE approval waits for bridged funds
- ๐ธ AAVE supply waits for approval
- ๐ค Withdrawal waits for aUSDC
๐ก๏ธ Cleanup Mechanism
If any step fails, cleanup instructions ensure funds are returned:
cleanUps: [{
chainId: base.id,
recipientAddress: eoa.address,
tokenAddress: usdcAddresses[base.id]
}]
โฝ Gas Payment
The orchestration is gasless because:
- ๐ฐ Gas paid using bridged USDC
- ๐ง MEE handles gas abstraction
- โ๏ธ Users only sign once
๐ญ Across Quote Service
Quote Parameters
interface SuggestedFeesParameters {
inputToken: Address // Source token address
outputToken: Address // Destination token address
originChainId: number // Source chain ID
destinationChainId: number // Destination chain ID
amount: bigint // Amount to bridge
// Optional parameters
depositor?: Address // Override depositor address
recipient?: Address // Override recipient address
message?: Hex // Cross-chain message
referrer?: Address // For referral tracking
}
Fee Structure
interface SuggestedFeesReturnType {
totalRelayFee: Fee // Total relayer compensation
relayerCapitalFee: Fee // Fee for fronting capital
relayerGasFee: Fee // Gas reimbursement
lpFee: Fee // Liquidity provider fee
timestamp: bigint // Quote timestamp
fillDeadline: bigint // Deadline for fill
limits: { // Deposit limits
minDeposit: bigint
maxDeposit: bigint
maxDepositInstant: bigint
recommendedDepositInstant: bigint
}
}
๐ก Best Practices
- ๐ Always use runtime balances for cross-chain operations
- ๐ก๏ธ Include cleanup instructions for failure scenarios
- โฐ Set reasonable time bounds (60 seconds recommended)
- ๐งช Test on testnets first before mainnet deployment
- ๐ Monitor transactions using MEE Scan
- ๐ธ Check deposit limits before bridging large amounts
- ๐ Secure your API keys in environment variables
- โ ๏ธ Validate fee amounts to ensure they're reasonable
๐จ Error Handling
try {
const fees = await getAcrossSuggestedFees(parameters)
// Check if amount is too low
if (fees.isAmountTooLow) {
throw new Error('โ Amount below minimum deposit')
}
// Check deposit limits
if (inputAmount > fees.limits.maxDepositInstant) {
console.warn('โ ๏ธ Amount exceeds instant deposit limit')
}
// ... orchestration logic
} catch (error) {
if (error.message.includes('HTTP error')) {
console.error('โ Failed to get Across quote')
}
console.error('๐จ Orchestration failed:', error)
}
๐ Advanced Features
๐จ Cross-Chain Messaging
Include messages with your bridge:
const fees = await getAcrossSuggestedFees({
// ... other params
message: '0x1234...', // Encoded message
})
// Include message in deposit
args: [
// ... other args
message // Pass to depositV3
]
๐ฐ Referral Tracking
Track referrals for analytics:
const fees = await getAcrossSuggestedFees({
// ... other params
referrer: '0xYourReferrerAddress',
})
๐ฏ Exclusive Relayers
Specify exclusive relayers for priority fills:
args: [
// ... other args
'0xRelayerAddress', // exclusiveRelayer
// ...
300, // exclusivityDeadline (5 min)
]
๐ Multi-Chain Cleanups
cleanUps: [
{
chainId: optimism.id,
recipientAddress: eoa.address,
tokenAddress: usdcOptimism
},
{
chainId: base.id,
recipientAddress: eoa.address,
tokenAddress: usdcBase
}
]
๐ Fee Analysis
// Format fees for display
import { formatAcrossFeePercentage } from './across-quote-service'
console.log(`
๐ฐ Total Fee: ${formatAcrossFeePercentage(fees.totalRelayFee.pct)}%
โฝ Gas Fee: ${formatAcrossFeePercentage(fees.relayerGasFee.pct)}%
๐ต LP Fee: ${formatAcrossFeePercentage(fees.lpFee.pct)}%
`)
๐ Monitoring & Analytics
Track your orchestrations:
const meeScanUrl = getMeeScanLink(hash)
console.log(`๐ Track transaction: ${meeScanUrl}`)
Monitor Across fills:
// Check fill status on Across Explorer
const acrossExplorerUrl = `https://app.across.to/transactions?deposit=${transactionHash}`
console.log(`๐ Track on Across: ${acrossExplorerUrl}`)
๐ Supported Chains & Features
Across supports ultra-fast bridging across major chains:
- โ๏ธ Chains: Ethereum, Arbitrum, Optimism, Base, Polygon, zkSync, and more
- ๐ฑ Features: Optimistic filling, single liquidity pool, UMA oracle validation
- โฑ๏ธ Speed: Usually completes in under 2 minutes
- ๐ธ Capital Efficiency: Best rates due to unified liquidity
๐ Conclusion
This integration combines Across's ultra-fast optimistic bridging with Biconomy MEE's orchestration capabilities to deliver:
- โก Lightning-fast cross-chain transfers (under 2 minutes)
- โฝ Completely gasless experience
- โ๏ธ Single signature for complex flows
- ๐ก๏ธ Built-in failure protection
- ๐ฐ Capital-efficient pricing
The result is a seamless DeFi experience that abstracts away all the complexity of cross-chain operations while providing the fastest possible bridging.
๐ Resources
- ๐ Across Protocol Documentation
- ๐ง Across API Reference
- ๐ฌ Across Discord
- ๐ Across Explorer