Embedded Actions
Transfer ERC-20 Tokens After Swap
Transfer swapped ERC-20 tokens to a different address on the destination chain.
This example demonstrates a common post-swap operation: transferring the newly acquired ERC-20 tokens to a different address on the destination chain. The swap lands tokens in the MulticallHandler, which then executes the transfer.
Action Configuration
The action targets the output token's ERC-20 contract and calls transfer(). The amount is dynamically populated with the actual swap output balance.
{
"actions": [
{
"target": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"functionSignature": "function transfer(address to, uint256 value)",
"args": [
{
"value": "0x718648C8c531F91b528A7757dD2bE813c3940608",
"populateDynamically": false
},
{
"value": "0",
"populateDynamically": true,
"balanceSourceToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
}
],
"value": "0",
"isNativeTransfer": false,
"populateCallValueDynamically": false
}
]
}How It Works
target— The ERC-20 token contract on the destination chain (USDC on Arbitrum in this example)functionSignature— Standard ERC-20transfer(address, uint256)function- First arg (recipient) — Static address where tokens should be sent.
populateDynamically: falsemeans this value is used as-is - Second arg (amount) — Set to
populateDynamically: truewithbalanceSourceTokenpointing to the same token contract. At execution time, the MulticallHandler replaces"0"with the actual token balance received from the swap
Full Example
import { createWalletClient, createPublicClient, http, parseUnits } from "viem";
import { arbitrum } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const walletClient = createWalletClient({
account,
chain: arbitrum,
transport: http(),
});
const publicClient = createPublicClient({
chain: arbitrum,
transport: http(),
});
// MulticallHandler on Arbitrum
const MULTICALL_HANDLER = "0x924a9f036260DdD5808007E1AA95f08eD08aA569";
// USDC on Arbitrum
const USDC_ARBITRUM = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
// Final recipient for the tokens
const FINAL_RECIPIENT = "0x718648C8c531F91b528A7757dD2bE813c3940608";
async function swapAndTransfer() {
const params = new URLSearchParams({
tradeType: "exactInput",
originChainId: "8453", // Base
destinationChainId: "42161", // Arbitrum
inputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
outputToken: USDC_ARBITRUM,
amount: parseUnits("100", 6).toString(),
depositor: account.address,
recipient: MULTICALL_HANDLER,
});
const response = await fetch(
`https://app.across.to/api/swap/approval?${params}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
actions: [
{
target: USDC_ARBITRUM,
functionSignature: "function transfer(address to, uint256 value)",
args: [
{ value: FINAL_RECIPIENT, populateDynamically: false },
{
value: "0",
populateDynamically: true,
balanceSourceToken: USDC_ARBITRUM,
},
],
value: "0",
isNativeTransfer: false,
populateCallValueDynamically: false,
},
],
}),
}
);
const quote = await response.json();
// Execute approvals
if (quote.approvalTxns?.length) {
for (const approvalTx of quote.approvalTxns) {
const hash = await walletClient.sendTransaction({
to: approvalTx.to,
data: approvalTx.data,
});
await publicClient.waitForTransactionReceipt({ hash });
}
}
// Execute swap
const hash = await walletClient.sendTransaction({
to: quote.swapTx.to,
data: quote.swapTx.data,
value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n,
});
console.log("Swap + transfer tx:", hash);
}
swapAndTransfer();The recipient in the query params is the MulticallHandler contract, not the final token recipient. The MulticallHandler receives the tokens, then executes your action to transfer them to the actual recipient.