Skip to main content

5.4 Advanced: Swap [ERC20 to ERC20]

5.4.1 Single Output Multi-Call Swap Transaction

In this section, we explore an advanced example of a single-output transaction involving multiple smart contract calls (multi-call) within a single output. This example demonstrates how the SurferMonkey SDK can be used to perform a swap on UniSwap from USDC to Wrapped Bitcoin (wBTC), and also a direct asset transfer, while executing multiple steps within a single atomic transaction.

Overview

This advanced scenario demonstrates the flexibility of SurferMonkey for managing multi-call processes, including ERC20 token approvals, interaction with the UniSwap Permit2, executing a swap, and transferring funds. The example aims to highlight the ease of integrating complex blockchain operations into a single user message configuration, which maintains privacy, efficiency, and reliability.

We will perform the following operations:

  • Approve USDC Spend: The SurferMonkey Proxy approves the UniSwap Permit2 contract to spend USDC.
  • Approve Permit2 Standard: The Permit2 contract allows the UniSwap Router to utilize our tokens.
  • Execute Swap: Perform a swap from USDC to wBTC using the UniSwap Router.
  • Transfer Funds: Transfer remaining USDC to another recipient.

Configuration Steps

1. Define Input Transaction

The input transaction specifies the total amount to be locked into SurferMonkey, which will be used across multiple smart contract calls.

const amount = "2000000000"; // 2K USDC
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 a specified amount of USDC on behalf of the SurferMonkey Proxy.

info

Note: The msg.sender in this context refers to the Proxy Smart Contract address.

const amountSwap = "1000000000"; // 1K USDC
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, amountSwap] // Approve Permit2 contract to spend 1K USDC
}
};

Explanation: This call interacts with the USDC token contract to approve spending by the UniSwap Permit2 Smart Contract. This step is essential for allowing UniSwap to utilize USDC 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 tokens from Permit2 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 authorizes the UniSwap Router to interact with the USDC managed by the Permit2 contract, allowing for subsequent swap operations.

4. Prepare UniSwap Swap Message

Using the UniSwap RoutePlanner, prepare the swap message, which includes creating the swap path and defining the swap details.

// Prepare Uniswap Swap payload
const planner = new RoutePlanner();
const path = aux.encodePathExactInput([INPUT_TOKEN, OUTPUT_TOKEN]); // Encode swap path from USDC to wBTC
planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
SWAP_RECIPIENT, // Recipient address for the swap (Charlie)
amountSwap, // Amount to swap (1K USDC)
"1000", // Minimum output tokens (in smallest units)
path, // Swap path from USDC to wBTC
true // Additional UniSwap parameter
]);

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 executes the swap, converting USDC into wBTC using the UniSwap Router. The parameters include the pre-configured swap message prepared in the previous step.

6. Set Up Fourth Smart Contract Call (Transfer Remaining USDC)

Finally, execute a direct ERC20 transfer to send the remaining USDC from SurferMonkey Proxy to Bob.

const amountTransfer = "1000000000"; // 1K USDC
const smartContractCallFour = {
payloadAmountNative: "0",
targetSC: INPUT_TOKEN, // Address of the USDC token contract
payloadObject: {
functionHeader: "function transfer(address recipient, uint256 amount)",
functionName: "transfer",
payloadParmsArr: [TRANSFER_RECIPIENT, amountTransfer] // Transfer 1K USDC to Bob
}
};

Explanation: This step transfers the remaining 1,000 USDC 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 example:

const { CommandType, RoutePlanner } = require("routePlanner.js") // UniSwap Route Planner JS

const INPUT_TOKEN = "0xUSDCAddress"; // USDC address
const OUTPUT_TOKEN = "0xwBTC"; // wBTC 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 = "2000000000"; // 2K USDC
const amountSwap = "1000000000"; // 1K USDC
const amountTransfer = "1000000000"; // 1K 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,
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,
UNISWAP_ROUTER_MAIN,
"99999999999999999999999999999999999999999999999",
DEADLINE + ""
]
}
};

// Create Swap message
const planner = new RoutePlanner();
const path = aux.encodePathExactInput([INPUT_TOKEN, OUTPUT_TOKEN]);
planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
SWAP_RECIPIENT,
amountSwap,
"1000",
path,
true
]);

// 3. Execute the UniSwap swap
const smartContractCallThree = {
payloadAmountNative: "0",
targetSC: UNISWAP_ROUTER_MAIN,
payloadObject: {
functionHeader: "function execute(bytes calldata commands, bytes[] calldata inputs)",
functionName: "execute",
payloadParmsArr: [planner.commands, planner.inputs]
}
};

// 4. Direct ERC20 transfer of the remaining USDC funds
const smartContractCallFour = {
payloadAmountNative: "0",
targetSC: INPUT_TOKEN,
payloadObject: {
functionHeader: "function transfer(address recipient, uint256 amount)",
functionName: "transfer",
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
};