cross-chain-asset-transfer
Transfer tokens between blockchain networks using Hyperlane's cross-chain infrastructure. Move assets like USDC from Ethereum to Polygon or execute multi-chain transfers in a single operation.
Instructions
Transfers tokens/assets between multiple blockchain networks using Hyperlane's cross-chain infrastructure.
FUNCTIONALITY: • Moves tokens from one blockchain to another (e.g., USDC from Ethereum to Polygon) • Supports sequential transfers across multiple chains in a single operation • Handles various token types including native tokens, ERC20 tokens, and synthetic tokens
PREREQUISITES:
• A warp route must exist for the specified token symbol and chain combination
• If no warp route exists, deploy one first using the deploy-warp-route tool
• Sufficient token balance on the origin chain
• Sufficient gas tokens on all involved chains for transaction fees
PARAMETERS: • symbol: The token identifier (e.g., "USDC", "ETH", "WBTC") • chains: Array of blockchain names in transfer order (e.g., ["ethereum", "polygon", "arbitrum"]) • amount: Token amount in wei or smallest token units (e.g., "1000000" for 1 USDC with 6 decimals) • recipient: Destination wallet address (defaults to sender if not specified)
OUTPUT: • Returns transaction hashes and message IDs for each cross-chain transfer • Each transfer between adjacent chains generates one transaction • Use message IDs to track delivery status across chains
EXAMPLE USE CASES: • Bridge USDC from Ethereum to Polygon • Multi-hop transfer: ETH from Ethereum → Arbitrum → Base • Cross-chain token arbitrage or yield farming
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| symbol | Yes | Token symbol to transfer | |
| chains | Yes | Chains to transfer asset between in order of transfer | |
| amount | Yes | Amount to transfer (in wei or token units) | |
| recipient | No | Recipient address | 0x1Be31A94361a391bBaFB2a4CCd704F57dc04d4bb |
Implementation Reference
- src/index.ts:271-450 (registration)Registration of the 'cross-chain-asset-transfer' MCP tool, including description, Zod input schema, and wrapper handler that validates warp routes, sets up MultiProvider, finds WarpCoreConfig, and delegates to assetTransfer helper.server.tool( 'cross-chain-asset-transfer', "Transfers tokens/assets between multiple blockchain networks using Hyperlane's cross-chain infrastructure.\n\n" + 'FUNCTIONALITY:\n' + '• Moves tokens from one blockchain to another (e.g., USDC from Ethereum to Polygon)\n' + '• Supports sequential transfers across multiple chains in a single operation\n' + '• Handles various token types including native tokens, ERC20 tokens, and synthetic tokens\n\n' + 'PREREQUISITES:\n' + '• A warp route must exist for the specified token symbol and chain combination\n' + '• If no warp route exists, deploy one first using the `deploy-warp-route` tool\n' + '• Sufficient token balance on the origin chain\n' + '• Sufficient gas tokens on all involved chains for transaction fees\n\n' + 'PARAMETERS:\n' + '• symbol: The token identifier (e.g., "USDC", "ETH", "WBTC")\n' + '• chains: Array of blockchain names in transfer order (e.g., ["ethereum", "polygon", "arbitrum"])\n' + '• amount: Token amount in wei or smallest token units (e.g., "1000000" for 1 USDC with 6 decimals)\n' + '• recipient: Destination wallet address (defaults to sender if not specified)\n\n' + 'OUTPUT:\n' + '• Returns transaction hashes and message IDs for each cross-chain transfer\n' + '• Each transfer between adjacent chains generates one transaction\n' + '• Use message IDs to track delivery status across chains\n\n' + 'EXAMPLE USE CASES:\n' + '• Bridge USDC from Ethereum to Polygon\n' + '• Multi-hop transfer: ETH from Ethereum → Arbitrum → Base\n' + '• Cross-chain token arbitrage or yield farming', { symbol: z.string().describe('Token symbol to transfer'), chains: z .array(z.string()) .describe('Chains to transfer asset between in order of transfer'), amount: z.string().describe('Amount to transfer (in wei or token units)'), recipient: z .string() .length(42) .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid EVM address') .optional() .default(signer.address) .describe('Recipient address'), }, async ({ symbol, chains, amount, recipient }) => { server.server.sendLoggingMessage({ level: 'info', data: `Starting cross-chain asset transfer... Parameters: symbol=${symbol}, chains=${chains.join( ', ' )}, amount=${amount}, recipient=${recipient}`, }); // Fetch warp route config from registry const warpRoutes = await registry.getWarpRoutesBySymbolAndChains( symbol, chains ); if (!warpRoutes || warpRoutes.length === 0) { return { content: [ { type: 'text', text: `No warp route config found for symbol "${symbol}" and chains [${chains.join( ', ' )}]. Please deploy a warp route first using the 'deploy-warp-route' tool.`, }, ], }; } const chainMetadata: ChainMap<ChainMetadata> = Object.fromEntries( await Promise.all( chains.map(async (chain) => [ chain, (await registry.getChainMetadata(chain))!, ]) ) ); const multiProvider = new MultiProvider(chainMetadata, { signers: Object.fromEntries(chains.map((chain) => [chain, signer])), providers: Object.fromEntries( await Promise.all( chains.map(async (chain) => [ chain, new ethers.providers.JsonRpcProvider( chainMetadata[chain].rpcUrls[0].http ), ]) ) ), }); server.server.sendLoggingMessage({ level: 'info', data: `MultiProvider initialized with chains: ${JSON.stringify( multiProvider, null, 2 )}`, }); let warpCoreConfig: WarpCoreConfig | null = null; for (const route of warpRoutes) { const warpCore = WarpCore.FromConfig( MultiProtocolProvider.fromMultiProvider(multiProvider), route ); const tokensForRoute = warpCore.getTokensForRoute(chains[0], chains[1]); if (tokensForRoute.length > 0) { warpCoreConfig = route; break; } } if (!warpCoreConfig) { return { content: [ { type: 'text', text: `No valid warp core config found for symbol "${symbol}" and chains [${chains.join( ', ' )}]. Please deploy a warp route first using the 'deploy-warp-route' tool.`, }, ], }; } else { server.server.sendLoggingMessage({ level: 'info', data: `Found warp core config: ${JSON.stringify( warpCoreConfig, null, 2 )}`, }); } server.server.sendLoggingMessage({ level: 'info', data: 'Initiating asset transfer...', }); const deliveryResult = await assetTransfer({ warpCoreConfig, chains, amount, recipient, multiProvider, }); if (!deliveryResult || deliveryResult.length !== chains.length - 1) { return { content: [ { type: 'text', text: `Error in asset transfer. No delivery result couldn't be generated`, }, ], }; } server.server.sendLoggingMessage({ level: 'info', data: 'Asset transfer completed successfully', }); return { content: [ { mimeType: 'application/json', type: 'text', text: JSON.stringify( deliveryResult.map(([dispatchTx, message]) => ({ transactionHash: dispatchTx.transactionHash, messageId: message.id, })), null, 2 ), }, ], }; } );
- src/index.ts:296-308 (schema)Zod schema for input parameters: symbol, chains array, amount, optional recipient address.{ symbol: z.string().describe('Token symbol to transfer'), chains: z .array(z.string()) .describe('Chains to transfer asset between in order of transfer'), amount: z.string().describe('Amount to transfer (in wei or token units)'), recipient: z .string() .length(42) .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid EVM address') .optional() .default(signer.address) .describe('Recipient address'),
- src/index.ts:310-449 (handler)MCP tool handler function: logs params, fetches warp routes, sets up providers and metadata, selects warpCoreConfig, calls assetTransfer helper, processes and returns results as JSON.async ({ symbol, chains, amount, recipient }) => { server.server.sendLoggingMessage({ level: 'info', data: `Starting cross-chain asset transfer... Parameters: symbol=${symbol}, chains=${chains.join( ', ' )}, amount=${amount}, recipient=${recipient}`, }); // Fetch warp route config from registry const warpRoutes = await registry.getWarpRoutesBySymbolAndChains( symbol, chains ); if (!warpRoutes || warpRoutes.length === 0) { return { content: [ { type: 'text', text: `No warp route config found for symbol "${symbol}" and chains [${chains.join( ', ' )}]. Please deploy a warp route first using the 'deploy-warp-route' tool.`, }, ], }; } const chainMetadata: ChainMap<ChainMetadata> = Object.fromEntries( await Promise.all( chains.map(async (chain) => [ chain, (await registry.getChainMetadata(chain))!, ]) ) ); const multiProvider = new MultiProvider(chainMetadata, { signers: Object.fromEntries(chains.map((chain) => [chain, signer])), providers: Object.fromEntries( await Promise.all( chains.map(async (chain) => [ chain, new ethers.providers.JsonRpcProvider( chainMetadata[chain].rpcUrls[0].http ), ]) ) ), }); server.server.sendLoggingMessage({ level: 'info', data: `MultiProvider initialized with chains: ${JSON.stringify( multiProvider, null, 2 )}`, }); let warpCoreConfig: WarpCoreConfig | null = null; for (const route of warpRoutes) { const warpCore = WarpCore.FromConfig( MultiProtocolProvider.fromMultiProvider(multiProvider), route ); const tokensForRoute = warpCore.getTokensForRoute(chains[0], chains[1]); if (tokensForRoute.length > 0) { warpCoreConfig = route; break; } } if (!warpCoreConfig) { return { content: [ { type: 'text', text: `No valid warp core config found for symbol "${symbol}" and chains [${chains.join( ', ' )}]. Please deploy a warp route first using the 'deploy-warp-route' tool.`, }, ], }; } else { server.server.sendLoggingMessage({ level: 'info', data: `Found warp core config: ${JSON.stringify( warpCoreConfig, null, 2 )}`, }); } server.server.sendLoggingMessage({ level: 'info', data: 'Initiating asset transfer...', }); const deliveryResult = await assetTransfer({ warpCoreConfig, chains, amount, recipient, multiProvider, }); if (!deliveryResult || deliveryResult.length !== chains.length - 1) { return { content: [ { type: 'text', text: `Error in asset transfer. No delivery result couldn't be generated`, }, ], }; } server.server.sendLoggingMessage({ level: 'info', data: 'Asset transfer completed successfully', }); return { content: [ { mimeType: 'application/json', type: 'text', text: JSON.stringify( deliveryResult.map(([dispatchTx, message]) => ({ transactionHash: dispatchTx.transactionHash, messageId: message.id, })), null, 2 ), }, ], }; }
- src/assetTransfer.ts:17-58 (helper)Core helper function that performs sequential cross-chain asset transfers by looping over chain pairs and executing delivery for each hop using executeDelivery.export async function assetTransfer({ warpCoreConfig, chains, amount, recipient, multiProvider, }: { warpCoreConfig: WarpCoreConfig; chains: ChainName[]; amount: string; recipient?: string; multiProvider: MultiProvider; }): Promise<[ContractReceipt, DispatchedMessage][]> { const results: [ContractReceipt, DispatchedMessage][] = []; for (let i = 0; i < chains.length; i++) { const origin = chains[i]; const destination = chains[i + 1]; if (destination) { const deliveryResult = await timeout( executeDelivery({ origin, destination, warpCoreConfig, amount, recipient, multiProvider, }), 120_000, 'Timed out waiting for messages to be delivered' ); if (deliveryResult) { const [dispatchTx, message] = deliveryResult; results.push([dispatchTx, message]); } else { break; } } } return results; }
- src/assetTransfer.ts:60-143 (helper)Helper function that executes a single hop transfer: initializes WarpCore, selects token, validates, prepares and sends transfer transactions, extracts dispatch receipt and dispatched message.async function executeDelivery({ origin, destination, warpCoreConfig, amount, recipient, multiProvider, }: { origin: ChainName; destination: ChainName; warpCoreConfig: WarpCoreConfig; amount: string; recipient?: string; multiProvider: MultiProvider; }): Promise<[ContractReceipt, DispatchedMessage]> { const signer = multiProvider.getSigner(origin); const recipientSigner = multiProvider.getSigner(destination); const recipientAddress = await recipientSigner.getAddress(); const signerAddress = await signer.getAddress(); recipient ||= recipientAddress; const provider = multiProvider.getProvider(origin); const connectedSigner = signer.connect(provider); const warpCore = WarpCore.FromConfig( MultiProtocolProvider.fromMultiProvider(multiProvider), warpCoreConfig ); let token: Token; const tokensForRoute = warpCore.getTokensForRoute(origin, destination); if (tokensForRoute.length === 0) { // console.error(`No Warp Routes found from ${origin} to ${destination}`); throw new Error( `Error finding warp route.\n tokensForRoute: ${JSON.stringify( tokensForRoute )}` ); } else if (tokensForRoute.length === 1) { token = tokensForRoute[0]; } else { // console.info(`Please select a token from the Warp config`); // const routerAddress = await runTokenSelectionStep(tokensForRoute); // token = warpCore.findToken(origin, routerAddress)!; throw new Error('Multiple tokens found for route'); } const errors = await warpCore.validateTransfer({ originTokenAmount: token.amount(amount), destination, recipient, sender: signerAddress, }); if (errors) { console.error('Error validating transfer: ', JSON.stringify(errors)); } // TODO: override hook address for self-relay const transferTxs = await warpCore.getTransferRemoteTxs({ originTokenAmount: new TokenAmount(amount, token), destination, sender: signerAddress, recipient, }); const txReceipts: ContractReceipt[] = []; for (const tx of transferTxs) { if (tx.type === ProviderType.EthersV5) { const txResponse = await connectedSigner.sendTransaction(tx.transaction); const txReceipt = await multiProvider.handleTx(origin, txResponse); txReceipts.push(txReceipt); } } const dispatchTx = txReceipts[txReceipts.length - 1]; const messageIndex: number = 0; const message: DispatchedMessage = HyperlaneCore.getDispatchedMessages(dispatchTx)[messageIndex]; const parsed = parseWarpRouteMessage(message.parsed.body); return [dispatchTx, message]; }