Destination payload execution
Destination payload lets you execute arbitrary contract calls on the destination chain after a cross-chain transfer completes. This is useful for flows such as depositing to a vault, repaying a loan, or interacting with a protocol immediately after bridging/swapping.
How it works
When building a Bungee request, you can provide calldata that will be executed on the destination chain. It works for both same chain swaps as well as cross chain swaps.
If the below parameters are provided, Bungee will deliver assets on destination and then invoke the receiver contract with your destinationPayload
under the specified gas limit.
destinationPayload
: ABI-encoded calldata for the target contract on the destination chain- The
destinationPayload
is thecallData
parameter that will be passed to theexecuteData
function - Execution may fail due to invalid encoding: re-check
encodeAbiParameters
types match the receiver decode
- The
receiverAddress
: The execution target that will receive the calldata and perform the call- The
receiverAddress
needs to be a contract that implementsIBungeeExecutor
contract's interface for theexecuteData
function - Ensure the receiver contract trusts/can handle calls from the Bungee executor
- Validate that the token list and amounts align with your business logic
- The
destinationGasLimit
: Gas budget on destination to execute the payload (naming varies by API surface)- Complex receivers or multi-token logic require higher gas limits
- Execution may fail due to out of gas on destination: increase
destinationGasLimit
. Measure via local tests or simulations and add headroom
interface IBungeeExecutor {
function executeData(
bytes32 requestHash,
uint256[] calldata amounts,
address[] calldata tokens,
bytes memory callData
) external payable;
}
Implementation example
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";
async function getQuoteWithDestinationPayload() {
const quoteParams = {
userAddress: "0xYourUsersAddress",
originChainId: "8453", // Base
destinationChainId: "8453", // Base → Base example
inputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
outputToken: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", // USDT on Base
inputAmount: "10000000", // 10 USDC (6 decimals)
receiverAddress: contractAddress, // contract address that implements executeData
destinationPayload: "0x1234acbd", // calldata for the contract set on receiverAddress
destinationGasLimit: "100000", // gas ceiling for destination execution
};
// Build the URL with query parameters
const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/quote`;
const queryParams = new URLSearchParams(quoteParams);
const fullUrl = `${url}?${queryParams}`;
// Make the request
const response = await fetch(fullUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
const serverReqId = response.headers.get("server-req-id");
if (!data.success) {
throw new Error(
`Quote error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
);
}
}
Script
Executing a Custom Payload on Destination Using Viem
import {
createWalletClient,
createPublicClient,
http,
encodeAbiParameters,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base, optimism } from 'viem/chains';
// Configuration - Update these values
const PRIVATE_KEY = process.env.PRIVATE_KEY;
// Example receiver contract on Base that implements executeData(...)
// and sends funds back to the userAddress
// The example receiver decodes only an `address` and transfers the first `(token, amount)` pair
// https://basescan.org/address/0xC0b43F2B38CA47CC9e1b9697296716ebCF3D8177#code
const CONTRACT_ADDRESS = '0xC0b43F2B38CA47CC9e1b9697296716ebCF3D8177'; // ExecuteDestinationPayload
// Create account from private key
const account = privateKeyToAccount(PRIVATE_KEY);
// Create OP clients
const publicClient = createPublicClient({
chain: base,
transport: http(),
});
const walletpublicClient = createWalletClient({
account,
chain: base,
transport: http(),
});
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";
const quoteParamsERC20 = {
userAddress: account.address,
originChainId: 8453, // Base
inputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
//originChainId: 10, // OP
//inputToken: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", // USDC on OP
destinationChainId: 8453, // Base
//outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
outputToken: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", // USDT on Base
inputAmount: "3000000", // 3 USDC (6 decimals)
};
async function getQuote(params) {
const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/quote`;
const queryParams = new URLSearchParams(params);
const queryUrl = `${url}?${queryParams}`;
console.log(queryUrl);
const response = await fetch(queryUrl);
const data = await response.json();
const serverReqId = response.headers.get("server-req-id");
if (!data.success) {
throw new Error(
`Quote error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
);
}
if (!data.result.autoRoute) {
throw new Error(`No autoRoute available. server-req-id: ${serverReqId}`);
}
console.log(`server-req-id: ${serverReqId}`);
const quoteId = data.result.autoRoute.quoteId;
const requestType = data.result.autoRoute.requestType;
let witness = null;
let signTypedData = null;
if (data.result.autoRoute.signTypedData) {
signTypedData = data.result.autoRoute.signTypedData;
if (signTypedData.values && signTypedData.values.witness) {
witness = signTypedData.values.witness;
}
}
const approvalData = data.result.autoRoute.approvalData;
return {
quoteId,
requestType,
witness,
signTypedData,
approvalData,
fullResponse: data,
};
}
// Function to check and handle token approvals
async function checkAndApproveToken(approvalData) {
if (!approvalData || !approvalData.tokenAddress) {
console.log("No approval data found or required");
return;
}
console.log("\nChecking token approval...");
// ERC20 ABI for allowance and approve functions
const erc20Abi = [
{
inputs: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
],
name: "allowance",
outputs: [{ name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
name: "approve",
outputs: [{ name: "", type: "bool" }],
stateMutability: "nonpayable",
type: "function",
},
];
try {
const currentAllowance = await publicClient.readContract({
address: approvalData.tokenAddress,
abi: erc20Abi,
functionName: "allowance",
args: [
approvalData.userAddress,
approvalData.spenderAddress,
],
});
console.log(`Current allowance: ${currentAllowance}`);
console.log(`Required approval: ${approvalData.amount}`);
// Check if approval is needed
if (BigInt(currentAllowance) >= BigInt(approvalData.amount)) {
console.log("Sufficient allowance already exists.");
return;
}
console.log("Insufficient allowance. Approving tokens...");
// Send approval transaction
const hash = await walletpublicClient.writeContract({
address: approvalData.tokenAddress,
abi: erc20Abi,
functionName: "approve",
args: [approvalData.spenderAddress, approvalData.amount],
});
console.log(`Approval transaction sent: ${hash}`);
// Wait for transaction to be mined
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log(`Approval confirmed in block ${receipt.blockNumber}`);
return receipt;
} catch (error) {
console.error("Error in approval process:", error);
throw error;
}
}
async function viemSignTypedData(signTypedData) {
const signature = await account.signTypedData({
types: signTypedData.types,
primaryType: "PermitWitnessTransferFrom",
message: signTypedData.values,
domain: signTypedData.domain,
});
return signature;
}
async function submitSignedRequest(
requestType,
request,
userSignature,
quoteId
) {
const requestBody = {
requestType,
request,
userSignature,
quoteId,
};
const response = await fetch(`${BUNGEE_API_BASE_URL}/api/v1/bungee/submit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestBody),
});
const data = await response.json();
if (!data.success) {
throw new Error(`Submit error: ${data.error?.message || "Unknown error"}`);
}
const serverReqId = response.headers.get("server-req-id");
console.log(`server-req-id: ${serverReqId}`);
return data.result;
}
async function checkStatus(requestHash) {
const response = await fetch(
`${BUNGEE_API_BASE_URL}/api/v1/bungee/status?requestHash=${requestHash}`
);
const data = await response.json();
if (!data.success) {
throw new Error(`Status error: ${data.error?.message || "Unknown error"}`);
}
return data.result[0];
}
async function main() {
try {
quoteParamsERC20.destinationPayload = encodeAbiParameters(
[{ type: 'address' }],
[account.address] // In this usecase the Payload is just the address to forward the funds to
);
quoteParamsERC20.destinationGasLimit = "100000"; // Max gas value taken from the forge test --gas-report was 62471
quoteParamsERC20.receiverAddress = CONTRACT_ADDRESS; // Receiver Address is the contract that will receive the payload
const quote = await getQuote(quoteParamsERC20);
console.log(`Got quote ${quote.quoteId}`);
const { quoteId, requestType, witness, signTypedData } = quote;
console.log(JSON.stringify(quote.fullResponse, null, 2));
if (quote.approvalData) {
await checkAndApproveToken(quote.approvalData);
}
if (signTypedData && witness) {
// Sign the typed data
console.log("\n2. Signing typed data...");
const signature = await viemSignTypedData(signTypedData);
// Submit the signed request
console.log("\n3. Submitting signed request...");
const submitResult = await submitSignedRequest(
requestType,
witness,
signature,
quoteId
);
console.log(
"\n4. Submission complete:",
"\n- Hash:",
submitResult.requestHash,
"\n- Type:",
submitResult.requestType
);
// Check the status
// Wait for 5 seconds
console.log("\n4. Waiting for 5 seconds...");
let status;
do {
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log("\n5. Checking status...");
try {
status = await checkStatus(submitResult.requestHash);
console.log("- Status details:", status.bungeeStatusCode);
} catch (error) {
console.error(
"Failed to check status:",
error?.message || "Unknown error"
);
}
} while (status?.bungeeStatusCode !== 3);
console.log(
"\n6. Transaction complete:",
"\n- Hash:",
status.destinationData?.txHash || "Transaction hash not available"
);
} else {
console.log("No signature data available in the quote response");
}
} catch (error) {
console.error('Main execution error:', error);
}
}
// Run the script
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
Example Smart Contract Implementation
The smart contract below is for example purposes only. Please ensure you audit your contracts for production usage.
Executing a Custom Payload on Destination Smart Contract
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import {Ownable} from "solady/auth/Ownable.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
/**
* @title IBungeeExecutor
* @dev Interface for executing cross-chain data payloads
*/
interface IBungeeExecutor {
/**
* @notice Executes data payload with token transfers
* @param requestHash Unique identifier for the cross-chain request
* @param amounts Array of token amounts to process
* @param tokens Array of token addresses to process
* @param callData Encoded data containing execution parameters
*/
function executeData(
bytes32 requestHash,
uint256[] calldata amounts,
address[] calldata tokens,
bytes memory callData
) external payable;
}
/**
* @title ExecuteDestinationPayload
* @dev Contract for executing destination payloads in cross-chain transfers
* @notice This contract handles the final step of cross-chain transfers by sending tokens to recipients
*/
contract ExecuteDestinationPayload is IBungeeExecutor, Ownable {
address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
/**
* @notice Emitted when a destination payload is successfully executed
* @param requestHash Unique identifier for the cross-chain request
* @param amount Amount of tokens transferred to recipient
* @param token Address of the token transferred
* @param recipient Address that received the tokens
*/
event DestinationPayload(
bytes32 indexed requestHash, uint256 amount, address indexed token, address indexed recipient
);
/**
* @notice Thrown when input arrays have invalid lengths
* @dev Both amounts and tokens arrays must have length is 1
*/
error InvalidArrayLength();
/**
* @notice Thrown when trying to rescue zero amount
*/
error ZeroAmount();
/**
* @notice Thrown when trying to rescue to zero address
*/
error ZeroAddress();
/**
* @notice Constructor sets the initial owner
*/
constructor() {
_initializeOwner(msg.sender);
}
/**
* @notice Executes the destination payload by transferring tokens to the recipient
* @dev Transfers the first token/amount pair to the decoded recipient address
* @param requestHash Unique identifier for the cross-chain request
* @param amounts Array of token amounts (must have length = 1, only first element used)
* @param tokens Array of token addresses (must have length = 1, only first element used)
* @param callData ABI-encoded recipient address
* @custom:security Only the first elements of amounts and tokens arrays are used
* @custom:security For ETH transfers, uses safeTransferETH
* @custom:security For ERC20 transfers, uses safeTransfer from msg.sender
*/
function executeData(
bytes32 requestHash,
uint256[] calldata amounts,
address[] calldata tokens,
bytes memory callData
) external payable override {
// Check if array length is 1
if (amounts.length != 1 || tokens.length != 1) {
revert InvalidArrayLength();
}
// Decode recipient address from callData
address recipient = abi.decode(callData, (address));
// Transfer funds to user using first array elements
address token = tokens[0];
uint256 amount = amounts[0];
if (token == NATIVE_TOKEN) {
// Transfer ETH to recipient
SafeTransferLib.safeTransferETH(recipient, amount);
} else {
// Transfer ERC20 token from msg.sender to recipient
SafeTransferLib.safeTransfer(token, recipient, amount);
}
emit DestinationPayload(requestHash, amount, token, recipient);
}
/**
* @notice Rescues all ETH from the contract
* @dev Only callable by the owner
*/
function rescueAllETH() external onlyOwner {
uint256 balance = address(this).balance;
if (balance == 0) revert ZeroAmount();
SafeTransferLib.safeTransferETH(msg.sender, balance);
}
/**
* @notice Rescues all ERC20 tokens from the contract
* @dev Only callable by the owner
* @param token Address of the ERC20 token to rescue
*/
function rescueAllERC20(address token) external onlyOwner {
if (token == address(0)) revert ZeroAddress();
uint256 balance = SafeTransferLib.balanceOf(token, address(this));
if (balance == 0) revert ZeroAmount();
SafeTransferLib.safeTransfer(token, msg.sender, balance);
}
/**
* @notice Allows the contract to receive ETH
*/
receive() external payable {}
}