5.5 Advanced: Swap [Native to ERC20]
5.5.1 Single Output Multi-Call Swap Transaction with Native Asset
In this section, we explore an advanced example of a single-output transaction involving multiple smart contract calls (multi-call) within a single output, specifically swapping a native asset (ETH) for an ERC20 asset (USDC). This example demonstrates how the SurferMonkey SDK can handle the complexities of working with native assets, converting ETH to Wrapped ETH (wETH), performing a swap using UniSwap, and then transferring remaining funds, all within a single atomic transaction.
Overview
This advanced scenario demonstrates the flexibility of the SurferMonkey SDK in managing native asset swaps, including wrapping ETH, ERC20 approvals, and interactions with UniSwap. The example highlights how the SurferMonkey SDK can integrate complex blockchain operations into a single user message configuration, maintaining privacy, efficiency, and reliability.
The following operations will be performed:
- Wrap ETH to wETH: Convert ETH to wETH, as ERC20 tokens are required for swaps on UniSwap.
- Approve wETH Spend: The SurferMonkey Proxy approves the UniSwap Permit2 contract to spend wETH.
- Approve Permit2 Standard: The Permit2 contract gives approval to the UniSwap Router to utilize the wETH.
- Execute Swap: Perform a swap from wETH to USDC using the UniSwap Router.
- Transfer Funds: Transfer the remaining ETH to another recipient.
Configuration Steps
1. Define Input Transaction
The input transaction specifies the total amount of ETH to be locked into SurferMonkey, which will be used across multiple smart contract calls.
const amount = "2000000000000000000"; // 2 ETH
const inputTx = {
AMOUNT_TOTAL: amount, // Total amount of ETH to lock
ASSET_ID: INPUT_TOKEN // Native asset ID for ETH
};
2. Set Up First Smart Contract Call (Approve ERC20 Spend)
The first call allows the SurferMonkey Proxy to approve the UniSwap Permit2 contract to spend a specified amount of Wrapped ETH (wETH) on behalf of the SurferMonkey Proxy.
Note: The msg.sender
in this context refers to the Proxy Smart Contract address.
const amountSwap = "1000000000000000000"; // 1 ETH
const smartContractCallOne = {
payloadAmountNative: "0",
targetSC: INPUT_TOKEN_SWAP, // Address of the wETH token contract
payloadObject: {
functionHeader: "function approve(address spender, uint256 amount)",
functionName: "approve",
payloadParmsArr: [PEMIT_ADDR, amountSwap] // Approve Permit2 contract to spend 1 ETH (in wETH)
}
};
Explanation: This call interacts with the Wrapped ETH contract to approve spending by the UniSwap Permit2 Smart Contract, allowing UniSwap to utilize wETH during the swap.
3. Set Up Second Smart Contract Call (Approve Permit2 for UniSwap)
The second call allows the Permit2 contract to give approval to the UniSwap Router, enabling it to use wETH for the swap operation.
const smartContractCallTwo = {
payloadAmountNative: "0",
targetSC: PEMIT_ADDR, // Address of the Permit2 Smart Contract
payloadObject: {
functionHeader: "function approve(address token, address spender, uint160 amount, uint48 expiration)",
functionName: "approve",
payloadParmsArr: [
INPUT_TOKEN_SWAP, // Token address (wETH)
UNISWAP_ROUTER_MAIN, // UniSwap Router address
"99999999999999999999999999999999999999999999999", // Maximum amount to allow
DEADLINE + "" // Expiration deadline
]
}
};
Explanation: This step authorizes the UniSwap Router to interact with the wETH managed by the Permit2 contract, allowing for subsequent swap operations.
4. Prepare UniSwap Swap Message (Wrap ETH and Execute Swap)
Using the UniSwap RoutePlanner, prepare the swap message, which includes wrapping ETH to wETH and then performing the swap.
const planner = new RoutePlanner();
planner.addCommand(CommandType.WRAP_ETH, [PROXY_ADDRESS, amountSwap]); // Wrap ETH to wETH via UniSwap Commands
const auxPath = aux.encodePathExactInput([INPUT_TOKEN_SWAP, OUTPUT_TOKEN]); // Encode path swap from wETH to USDC
planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
SWAP_RECIPIENT, // Recipient address for the swap (Charlie)
amountSwap, // Amount to swap (1 ETH in wETH)
minTokens, // Minimum output tokens (in smallest units for USDC)
auxPath, // Swap path from wETH to USDC
true // Additional UniSwap parameter
]);
Explanation: The first command wraps ETH into wETH, which is needed for the swap as ETH lacks the ERC20 interface required by UniSwap. The second command then sets up the swap from wETH to USDC.
5. Set Up Third Smart Contract Call (Execute UniSwap Swap)
Create the third smart contract call to execute the swap on UniSwap.
Since this call involves locking ETH to obtain wETH, you must specify the amount of native asset being sent in the payloadAmountNative
field.
const smartContractCallThree = {
payloadAmountNative: amountSwap, // Native ETH amount used in wrapping
targetSC: UNISWAP_ROUTER_MAIN, // Address of the UniSwap Router
payloadObject: {
functionHeader: "function execute(bytes calldata commands, bytes[] calldata inputs)",
functionName: "execute",
payloadParmsArr: [planner.commands, planner.inputs] // Use the prepared UniSwap commands and inputs
}
};
Explanation: This call executes the swap, converting wETH into USDC using the UniSwap Router. It includes the previously configured commands to wrap ETH and perform the swap.
6. Set Up Fourth Smart Contract Call (Transfer Remaining ETH)
Finally, execute a direct transfer to send the remaining ETH from SurferMonkey Proxy to Bob.
As this call involves transferring a native asset (ETH), ensure the payloadAmountNative
field specifies the amount being transferred.
const amountTransfer = "1000000000000000000"; // 1 ETH
const smartContractCallFour = {
payloadAmountNative: amountTransfer, // Amount of ETH to transfer
targetSC: PROXY_ADDRESS, // SurferMonkey Proxy address
payloadObject: {
functionHeader: "function transferEth(address targetAddress, uint256 amount)",
functionName: "transferEth",
payloadParmsArr: [TRANSFER_RECIPIENT, amountTransfer] // Transfer 1 ETH to Bob
}
};
Explanation: This step transfers the remaining 1 ETH to Bob, completing the series of transactions within a single output.
7. Create User Message
Construct the user message object to include all the defined smart contract calls:
const userMerkleProof = await SurferMonkey.getInstitutionMembershipProof({
BACKEND_RPC,
userEOA: sourceAddrHex,
institutionLeaves: ["0xValidUserAddress1", "0xValidUserAddress2", sourceAddrHex]
});
const userMessage = {
userEOA: sourceAddrHex, // User EVM address
inputTx: inputTx,
outputTxArr: [
{
amountOutput: inputTx.AMOUNT_TOTAL, // Amount output equals the total locked amount
smartContractCalls: [
smartContractCallOne,
smartContractCallTwo,
smartContractCallThree,
smartContractCallFour
]
}
],
numberTxOut: 1, // Number of output transactions
pubKey: ["PubKeyString[0]", "PubKeyString[1]"],
chainID: "ChainIdentifier",
userMerkleProof: userMerkleProof
};
Full User Message Code Snippet
Below is the complete User Message configuration code for this single-output, multi-call swap Native to ERC20 example involving a native asset:
const { CommandType, RoutePlanner } = require("routePlanner.js"); // UniSwap Route Planner JS
const INPUT_TOKEN = "0x0000000000000000000000000000000000000000000000000000000000000000"; // Native asset ID for ETH
const INPUT_TOKEN_SWAP = WRAPPED_ETH_ETHEREUM_MAIN; // Wrapped ETH contract address
const OUTPUT_TOKEN = USDC_ADDR; // USDC contract address
const PEMIT_ADDR = "0xUniSwapPermit2Address"; // UniSwap Permit2 address
const UNISWAP_ROUTER_MAIN = "0xUniSwapRouterAddress"; // UniSwap Router address
const DEADLINE = 2000000000; // UniSwap Deadline for the Swap
const amount = "2000000000000000000"; // 2 ETH
const amountSwap = "1000000000000000000"; // 1 ETH
const amountTransfer = "1000000000000000000"; // 1 ETH
const minTokens = "1000000000"; // 6 decimals for USDC
const SWAP_RECIPIENT = "0xCharlieAddress"; // Charlie address
const TRANSFER_RECIPIENT = "0xBobAddress"; // Bob address
const inputTx = {
AMOUNT_TOTAL: amount,
ASSET_ID: INPUT_TOKEN
};
// 1. ERC20 Approve tokens from Proxy, to be spent by Permit2
const smartContractCallOne = {
payloadAmountNative: "0",
targetSC: INPUT_TOKEN_SWAP,
payloadObject: {
functionHeader: "function approve(address spender, uint256 amount)",
functionName: "approve",
payloadParmsArr: [PEMIT_ADDR, amountSwap]
}
};
// 2. UniSwap Permit2 Approve UniSwap router to manage tokens
const smartContractCallTwo = {
payloadAmountNative: "0",
targetSC: PEMIT_ADDR,
payloadObject: {
functionHeader: "function approve(address token, address spender, uint160 amount, uint48 expiration)",
functionName: "approve",
payloadParmsArr: [
INPUT_TOKEN_SWAP,
UNISWAP_ROUTER_MAIN,
"99999999999999999999999999999999999999999999999",
DEADLINE + ""
]
}
};
// Create Swap message
const planner = new RoutePlanner();
planner.addCommand(CommandType.WRAP_ETH, [PROXY_ADDRESS, amountSwap]); // Wrap ETH to wETH via UniSwap Commands
const auxPath = aux.encodePathExactInput([INPUT_TOKEN_SWAP, OUTPUT_TOKEN]); // Encode path swap from wETH to USDC
planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
SWAP_RECIPIENT,
amountSwap,
minTokens,
auxPath,
true
]);
// 3. Execute the UniSwap swap
const smartContractCallThree = {
payloadAmountNative: amountSwap,
targetSC: UNISWAP_ROUTER_MAIN,
payloadObject: {
functionHeader: "function execute(bytes calldata commands, bytes[] calldata inputs)",
functionName: "execute",
payloadParmsArr: [planner.commands, planner.inputs]
}
};
// 4. Direct transfer of remaining ETH funds
const smartContractCallFour = {
payloadAmountNative: amountTransfer,
targetSC: PROXY_ADDRESS,
payloadObject: {
functionHeader: "function transferEth(address targetAddress, uint256 amount)",
functionName: "transferEth",
payloadParmsArr: [TRANSFER_RECIPIENT, amountTransfer]
}
};
const userMerkleProof = await SurferMonkey.getInstitutionMembershipProof({
BACKEND_RPC,
userEOA: sourceAddrHex,
institutionLeaves: ["0xValidUserAddress1", "0xValidUserAddress2", sourceAddrHex]
});
// Final: Define User Message object
const userMessage = {
userEOA: sourceAddrHex,
inputTx: inputTx,
outputTxArr: [
{
amountOutput: inputTx.AMOUNT_TOTAL,
smartContractCalls: [
smartContractCallOne,
smartContractCallTwo,
smartContractCallThree,
smartContractCallFour
]
}
],
numberTxOut: 1,
pubKey: ["PubKeyString[0]", "PubKeyString[1]"],
chainID: "ChainIdentifier",
userMerkleProof: userMerkleProof
};