5.6 Advanced: Swap [ERC20 to Native]
5.6.1 Single Output Multi-Call Swap Transaction with Unwrapping
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 an ERC20 asset (USDC) for a native asset (ETH). This example showcases the SurferMonkey SDK's ability to handle the complexities involved in unwrapping Wrapped ETH (wETH) to ETH, managing token approvals, and interacting with UniSwap to achieve a seamless transaction that ends with the native asset being transferred to a recipient.
Overview
This advanced use case illustrates the SurferMonkey SDK's flexibility when converting an ERC20 asset to a native asset. The process includes swapping USDC for wETH, unwrapping wETH to obtain ETH, and transferring the ETH to a final recipient. The example abstracts the complexities of managing wETH balances and unwrapping, allowing developers to perform all these operations seamlessly through simple SDK calls.
Key Operations:
- Approve USDC Spend: The SurferMonkey Proxy approves the UniSwap Permit2 contract to spend USDC.
- Approve Permit2 Standard: The Permit2 contract authorizes the UniSwap Router to use USDC.
- Execute Swap: Perform a swap from USDC to wETH using the UniSwap Router, with the SurferMonkey Proxy as the recipient.
- Unwrap and Transfer ETH: Unwrap wETH to ETH and transfer the resulting ETH to the final recipient.
Configuration Steps
1. Define Input Transaction
The input transaction specifies the total amount of USDC to be locked into SurferMonkey, which will be used across multiple smart contract calls.
const amount = "20000000000"; // 20K USDC, 6 decimals
const inputTx = {
AMOUNT_TOTAL: amount, // Total amount of USDC in smallest units
ASSET_ID: INPUT_TOKEN // ERC20 token address for USDC
};
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 the specified amount of USDC on behalf of the SurferMonkey Proxy.
Note: The msg.sender
in this context refers to the Proxy Smart Contract address.
const smartContractCallOne = {
payloadAmountNative: "0",
targetSC: INPUT_TOKEN, // Address of the USDC token contract
payloadObject: {
functionHeader: "function approve(address spender, uint256 amount)",
functionName: "approve",
payloadParmsArr: [PEMIT_ADDR, amount] // Approve Permit2 contract to spend 20K USDC
}
};
Explanation: This call interacts with the USDC token contract to approve spending by the UniSwap Permit2 Smart Contract. This is a necessary step to allow UniSwap to utilize USDC during the swap.
3. Set Up Second Smart Contract Call (Approve Permit2 for UniSwap)
The second call authorizes the UniSwap Router to interact with the USDC managed by the Permit2 contract 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, // Token address (USDC)
UNISWAP_ROUTER_MAIN, // UniSwap Router address
"99999999999999999999999999999999999999999999999", // Maximum amount to allow
DEADLINE + "" // Expiration deadline
]
}
};
Explanation: This step allows the UniSwap Router to use the USDC managed by the Permit2 contract for the swap.
4. Prepare UniSwap Swap Message
Using the UniSwap RoutePlanner, prepare the swap message, which includes the swap from USDC to wETH, with the SurferMonkey Proxy as the recipient.
Set the SurferMonkey Proxy Smart Contract as the recipient to facilitate wETH unwrapping and subsequent ETH transfer to the final recipient.
const planner = new RoutePlanner();
const auxPath = aux.encodePathExactInput([INPUT_TOKEN, OUTPUT_TOKEN]); // Encode swap path from USDC to wETH
planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
SWAP_RECIPIENT, // Recipient address for the swap (SurferMonkey Proxy)
amount, // Amount to swap (20K USDC)
minTokens, // Minimum output tokens (in smallest units for wETH)
auxPath, // Swap path from USDC to wETH
true // Additional UniSwap parameter
]);
Explanation: The swap is set up such that the resulting wETH will be held by the SurferMonkey Proxy. This is required for the next step where the wETH is unwrapped to obtain ETH.
5. Set Up Third Smart Contract Call (Execute UniSwap Swap)
Create the third smart contract call to execute the swap on UniSwap.
const smartContractCallThree = {
payloadAmountNative: "0",
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 performs the swap from USDC to wETH. The recipient of the swapped asset (wETH) is the SurferMonkey Proxy.
6. Set Up Fourth Smart Contract Call (Unwrap and Transfer ETH)
In this step, we unwrap the wETH held by the SurferMonkey Proxy and transfer the resulting ETH to the final recipient. This process is abstracted for simplicity, allowing developers to call the built-in function transferEthFromAnUnwrapped
in one go.
Use the Proxy's in-built transferEthFromAnUnwrapped
function to unwrap wETH and transfer the resulting ETH to the final recipient.
const smartContractCallFour = {
payloadAmountNative: "0",
targetSC: PROXY_ADDRESS, // SurferMonkey Proxy address
payloadObject: {
functionHeader: "function transferEthFromAnUnwrapped(address targetAddress, address tokenWrapped)",
functionName: "transferEthFromAnUnwrapped",
payloadParmsArr: [TRANSFER_RECIPIENT, WRAPPED_ETH_ETHEREUM_MAIN] // Unwrap wETH and transfer ETH to recipient
}
};
Explanation: This built-in function unwraps all the wETH balance held by the SurferMonkey Proxy and sends the resulting ETH to the specified recipient address. The complexity of unwrapping and managing balances is abstracted away, simplifying the developer's experience.
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 example involving unwrapping an ERC20 asset to a native asset:
const { CommandType, RoutePlanner } = require("routePlanner.js"); // UniSwap Route Planner JS
const INPUT_TOKEN = USDC_ADDR; // USDC address
const OUTPUT_TOKEN = WRAPPED_ETH_ADDR; // Wrapped ETH 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 = "20000000000"; // 20K USDC, 6 decimals
const minTokens = 1; // Minimum output tokens for the swap
const SWAP_RECIPIENT = "0xAddressSurferMonkeyProxy"; // SurferMonkey Proxy address
const TRANSFER_RECIPIENT = "0xAddressFinalRecipient"; // Recipient 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,
payloadObject: {
functionHeader: "function approve(address spender, uint256 amount)",
functionName: "approve",
payloadParmsArr: [PEMIT_ADDR, amount]
}
};
// 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,
UNISWAP_ROUTER_MAIN,
"99999999999999999999999999999999999999999999999",
DEADLINE + ""
]
}
};
// Create Swap message
const planner = new RoutePlanner();
const auxPath = aux.encodePathExactInput([INPUT_TOKEN, OUTPUT_TOKEN]); // Encode path swap from USDC to wETH
planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
SWAP_RECIPIENT, // Recipient address for the swap (SurferMonkey Proxy)
amount, // Amount to swap (20K USDC)
minTokens, // Minimum output tokens (in smallest units for wETH)
auxPath, // Swap path from USDC to wETH
true // Additional UniSwap parameter
]);
// 3. UniSwap Swap ERC20 (USDC) to ERC20 (wETH) with the Proxy as recipient address
const smartContractCallThree = {
payloadAmountNative: "0",
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
}
};
// 5. Unwrap all the wETH from the Proxy, and transfer the ETH to the final recipient
const smartContractCallFour = {
payloadAmountNative: "0",
targetSC: PROXY_ADDRESS, // SurferMonkey Proxy address
payloadObject: {
functionHeader: "function transferEthFromAnUnwrapped(address targetAddress, address tokenWrapped)",
functionName: "transferEthFromAnUnwrapped",
payloadParmsArr: [TRANSFER_RECIPIENT, WRAPPED_ETH_ETHEREUM_MAIN] // Unwrap wETH and transfer ETH to recipient
}
};
const userMerkleProof = await SurferMonkey.getInstitutionMembershipProof({
BACKEND_RPC,
userEOA: sourceAddrHex,
institutionLeaves: ["0xValidUserAddress1", "0xValidUserAddress2", sourceAddrHex]
});
// Define the User Message
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
};