Exact output mode

In this example, we will conduct price query and swap with exact output amount in iZiSwap (Desire Mode) via UniversalQuoter and UniversalSwapRouter.

Here, quoter with exact output amount means pre-query amount of token need to pay given exact amount of desired token. For example, if you want to swap N ETH to 2000 USDC, the step is used to determine N.

swap with exact output amount means to invoke the real swap with the desired amount.

Quoter and swap are called throw 2 different contracts.

Suppose we want to swap test USDT token to test BNB token, where the test USDT is standard ERC-20 token deployed by us on BSC testnet and the test BNB is native token on BSC testnet.

The full example codes can be found in this link.

1. Some imports

These are some basic modules used in most examples.

1import {BaseChain, ChainId, initialChainTable, TokenInfoFormatted} from 'iziswap-sdk/lib/base/types'
2import {privateKey} from '../../.secret'
3import Web3 from 'web3';
4import { amount2Decimal } from 'iziswap-sdk/lib/token/token';
5import { BigNumber } from 'bignumber.js'

And these are some imports for UniversalQuoter and UniversalSwapRouter.

1import { getSwapExactOutputCall, getUniversalQuoterContract, getUniversalSwapRouterContract, quoteExactOutput } from 'iziswap-sdk/lib/universalRouter'
2import { SwapExactOutputParams } from 'iziswap-sdk/lib/universalRouter/types';

Here quoteExactOutput will return the amount of input token needed to pay (N), and getSwapExactOutputCall will return calling for swapping with exact amount of output (or we say desired) token.

2. Initialization

Following codes define some basic object like rpc, web3 and account which we may need. The network in this example is bsc testnet.

1const chain:BaseChain = initialChainTable[ChainId.BSCTestnet]
2// const rpc = 'https://bsc-dataseed2.defibit.io/'
3const rpc = 'https://bsc-testnet-rpc.publicnode.com';
4console.log('rpc: ', rpc)
5const web3 = new Web3(new Web3.providers.HttpProvider(rpc))
6const account =  web3.eth.accounts.privateKeyToAccount(privateKey)
7console.log('address: ', account.address)

Following codes define our UniversalQuoter contract object.

1const quoterAddress = '0xA55D57C62A1B998E4bDD956c31096b783b7ce1cF'
2const quoterContract = getUniversalQuoterContract(quoterAddress, web3)
3
4console.log('quoter address: ', quoterAddress)

Following codes define some test tokens deployed by us on bsc testnet.

 1const USDC = {
 2    chainId: chain.id,
 3    symbol: 'USDC',
 4    address: '0x876508837C162aCedcc5dd7721015E83cbb4e339',
 5    decimal: 6
 6} as TokenInfoFormatted;
 7
 8const USDT = {
 9    chainId: chain.id,
10    symbol: 'USDT',
11    address: '0x6AECfe44225A50895e9EC7ca46377B9397D1Bb5b',
12    decimal: 6
13} as TokenInfoFormatted;
14
15const BNB = {
16    chainId: chain.id,
17    symbol: 'BNB',
18    address: '0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd',
19    decimal: 18,
20} as TokenInfoFormatted;
21
22const WBNB = {
23    chainId: chain.id,
24    symbol: 'WBNB',
25    address: '0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd',
26    decimal: 18,
27} as TokenInfoFormatted;

And following codes define params for universal quoter.

 1const outputAmountDecimal = 0.2;
 2const outputAmount = new BigNumber(outputAmountDecimal).times(10 ** WBNB.decimal).toFixed(0)
 3
 4const quoterParams = {
 5    // note:
 6    //     if you want to pay via native BNB/WBNB,
 7    //         just change first token in tokenChain to BNB/WBNB
 8    //         like [BNB, ... other tokens] or [WBNB, ... other tokens]
 9    //     and if you want to buy BNB/WBNB,
10    //         you can just change the last token in tokenChain
11    //         to BNB/WBNB
12    //         like [... other tokens, BNB] or [... other tokens, WBNB]
13    //     Both of BNB and WBNB are defined in code above
14    // tokenChain: [BNB, USDC, USDT],
15    tokenChain: [USDT, USDC, BNB],
16
17    // fee percent of pool(tokenChain[i], tokenChain[i+1])
18    // 0.3 means fee tier of 0.3%
19    //     only need for V3Pool
20    //     for V2Pool, you can fill arbitrary value
21    feeTier: [0.04, 0],
22    // isV2[i] == true, means pool(tokenChain[i], tokenChain[i+1]) is a V2Pool
23    // otherwise, the corresponding pool is V3Pool
24    //     same length as feeTier
25    isV2: [false, true],
26
27    outputAmount,
28    // "maxInputAmount" is not used in quoter
29    //     and you can fill arbitrary value in quoter
30    maxInputAmount: '0',
31    // outChargeFeeTier% of trader's acquired token (outToken)
32    // will be additionally charged by universalRouter
33    // if outChargeFeeTier is 0.2, 0.2% of outToken will be additionally charged
34    // if outChargeFeeTier is 0, no outToken will be additionally charged
35    // outChargeFeeTier should not be greater than 5 (etc, 5%)
36    outChargeFeeTier: 0.2,
37} as SwapExactOutputParams;

In the above code, we ready to buy 0.2 test BNB. And then, we can see 3 arrays in quoterParams, tokenChain, feeTier, and isV2. These 3 lists together define the path we want to do price inquiry. We can see that there are 2 pools in our path. The first pool on the path is a V3-pool with pair of (USDT, USDC, 0.04%), here 0.04% is the fee rate of this pool. The second pool on the path is a V2-pool with pair of (USDC, BNB).

The fields of SwapExactOutputParams is explained in the following code.

 1export interface SwapExactOutputParams {
 2    // input: tokenChain.first()
 3    // output: tokenChain.last()
 4    tokenChain: TokenInfoFormatted[];
 5    // fee percent of pool(tokenChain[i], tokenChain[i+1]) 0.3 means 0.3%
 6    //     only need for V3Pool
 7    //     for V2Pool, you can fill arbitrary value
 8    feeTier: number[];
 9    // isV2[i] == true, means pool(tokenChain[i], tokenChain[i+1]) is a V2Pool
10    // otherwise, the corresponding pool is V3Pool
11    //     same length as feeTier
12    isV2: boolean[];
13    // 10-decimal format number, like 100, 150000, ...
14    // or hex format number start with '0x'
15    // amount = outputAmount / (10 ** outputToken.decimal)
16    outputAmount: string;
17    maxInputAmount: string;
18    // outChargeFeeTier% of trader's acquired token (outToken)
19    // will be additionally charged by universalRouter
20    // if outChargeFeeTier is 0.2, 0.2% of outToken will be additionally charged
21    // if outChargeFeeTier is 0, no outToken will be additionally charged
22    // outChargeFeeTier should not be greater than 5 (etc, 5%)
23    outChargeFeeTier: number;
24    recipient?: string;
25    deadline?: string;
26}

iZiSwap’s UniversalQuoter and UniversalSwap contracts support swap chain with multi V2 and V3 swap pools. For example, if you have some token0, and wants to get token3 through the path token0 -> (token0, token1, 0.05%) -> token1 -> (token1, token2, 0.3%) -> token2 -> (token2, token3) -> token3 (here, suppose the last pair (token2, token3) is a V2-pool), you should fill the tokenChain, feeTier and isV2 fields with following code

1// here, token0..3 are TokenInfoFormatted
2params.tokenChain = [token0, token1, token2, token3]
3params.feeChain = [0.05, 0.3, 0]
4params.isV2 = [false, false, true]

In general, the supported fee rates for V3-pools on the mainnet are 500 (0.05%), 3000 (0.3%), and 10000 (1%); and for the testnet are 400 (0.04%), 2000 (0.2%) and 10000 (1%). One needs to check if the choosen pool exists and has enough liquidity. The liquidity condition can be checked on the analytics page here .

3. Wrapped Native or Native token

In sdk-interfaces of UniversalQuoter and UniversalSwapRouter, if you want to pay or buy wrapped native or native token, just simply set tokenChain.first() or tokenChain.last() as wrapped native or native token. And we can also found that the only difference between wrapped native and native token is symbol field in TokenInfoFormatted

If you want to pay test BNB to buy test USDT, you can use following code. And in the following code, params is an instance of SwapExactOutputParams

1// objects in tokenChain are all TokenInfoFormatted
2// BNB and USDT are defined above in section 2.
3params.tokenChain = [BNB, ... /* other mid tokens*/, USDT]

If you want to pay test USDT to buy test WBNB, you can use following code.

1// objects in tokenChain are all TokenInfoFormatted
2// WBNB and USDT are defined above in section 2.
3params.tokenChain = [USDT, ... /* other mid tokens*/, WBNB]

more detail can be viewed in the code comment in section 2.

4. Out Token FeeTier

In the code in section 2, we can notice that the object quoterParams has a field named outChargeFeeTier.

This field specify the fee tier to charge from out token, in this example, the out token is test BNB.

If you specify outChargeFeeTier as 0.2, 0.2% of out token will be charged before sending to trader.

If you want 0.3% of out token is charged, you can use following code.

1params.outChargeFeeTier = 0.3

5. Use UniversalQuoter to pre-query amount of test USDT need to pay

1// whether limit maximum point range for each V3Pool in quoter
2const limit = true;
3const {inputAmount} = await quoteExactOutput(quoterContract, quoterParams, limit);
4
5const inputAmountDecimal = amount2Decimal(new BigNumber(inputAmount), USDT)
6
7console.log(' input amount decimal: ', inputAmountDecimal)
8console.log(' output amount decimal: ', outputAmountDecimal)

In the above code, we are ready to buy 0.2 test BNB (decimal amount, and value of outputAmountDecimal has been defined in section 2). We simply call function quoteExactOutput to get the amount of test USDT need to pay. The function quoteExactOutput need 3 params:

    • quoterContract: obtained through getUniversalQuoterContract in section 2

    • a quoterParams instance: obtained in section 2

    • limit: a boolean, true if we want to limit point range (no more than 10000) walked through in V3 pools during quoting, and false if we donot limit it.

Now we have finished the Quoter part.

6. Use UniversalSwap to actually pay test token USDT to get test BNB

First, we use getSwapContract to get the Swap contract

1const swapAddress = '0x8684E397A84D718dD65da5938B6985BA60C957c5' // Swap contract on BSC testnet
2const swapContract = getUniversalSwapRouterContract(swapAddress, web3)

Second, use getSwapExactInputCall to get calling (transaction handler) of swap:

 1const swapParams = {
 2    ...quoterParams,
 3    // slippery is 1.5%
 4    maxInputAmount: new BigNumber(inputAmount).times(1.015).toFixed(0)
 5} as SwapExactOutputParams
 6
 7const gasPrice = '5000000000'
 8
 9const {calling: swapCalling, options} = getSwapExactOutputCall(
10    swapContract,
11    account.address,
12    chain,
13    swapParams,
14    gasPrice
15)

In the above code, we ready to buy 0.2 test BNB (decimal amount). We simply call function getSwapExactOutputCall to get acquired amount of token test BNB. The params needed by function getSwapExactOutputCall can be viewed in the following code:

 1/**
 2* @param universalSwapRouter, universal swap router contract, can be obtained through getUniversalSwapRouterContract(...)
 3* @param account, address of user
 4* @param chain, object of BaseChain, describe which chain we are using
 5* @param params, some settings of this swap, including swapchain, input amount, min required output amount
 6* @param gasPrice, gas price of this swap transaction
 7* @return calling, calling of this swap transaction
 8* @return options, options of this swap transaction, used in sending transaction
 9*/
10export const getSwapExactOutputCall = (
11    universalSwapRouter: Contract<ContractAbi>,
12    account: string,
13    chain: BaseChain,
14    params: SwapExactOutputParams,
15    gasPrice: number | string
16) : {calling: any, options: any}

SwapExactOutputParams has been explained in section 2

We usually keep outChargeFeeTier, tokenChain, feeTier, isV2 unchanged from “quoterParams”, expect maxInputAmount. And Usually we can fill SwapExactOutputParams through following code,

1const swapParams = {
2    ...quoterParams,
3    // slippery is 1.5%
4    maxInputAmount: new BigNumber(inputAmount).times(1.015).toFixed(0)
5} as SwapExactOutputParams

Notice that in this example, input token (test USDT) is ERC20 token and output token (test BNB) is a native token. However, if you want to pay or receive wrapped native (ERC20) or native token, you can refer to section 3

7. Approve (skip if you pay native token directly)

Before sending transaction or estimating gas, you need to approve contract Swap to have authority to spend your token. Since the contract need to transfer some input token to the pool.

If the allowance is enough or the input token is chain gas token, just skip this step.

 1// the approve interface abi of erc20 token
 2const erc20ABI = [{
 3  "inputs": [
 4    {
 5      "internalType": "address",
 6      "name": "spender",
 7      "type": "address"
 8    },
 9    {
10      "internalType": "uint256",
11      "name": "amount",
12      "type": "uint256"
13    }
14  ],
15  "name": "approve",
16  "outputs": [
17    {
18      "internalType": "bool",
19      "name": "",
20      "type": "bool"
21    }
22  ],
23  "stateMutability": "nonpayable",
24  "type": "function"
25}];
26// if input token is not chain token (BNB on BSC or ETH on Ethereum...), we need transfer input token to pool
27// otherwise we can skip following codes
28{
29    const usdtContract = new web3.eth.Contract(erc20ABI, USDT.address);
30    // you could approve a very large amount (much more greater than amount to transfer),
31    // and don't worry about that because swapContract only transfer your token to pool with amount you specified and your token is safe
32    // then you do not need to approve next time for this user's address
33    const approveCalling = usdtContract.methods.approve(
34        swapAddress,
35        "0xffffffffffffffffffffffffffffffff"
36    );
37    // estimate gas
38    const gasLimit = await approveCalling.estimateGas({from: account})
39    // then send transaction to approve
40    // you could simply use followiing line if you use metamask in your frontend code
41    // otherwise, you should use the function "web3.eth.accounts.signTransaction"
42    // notice that, sending transaction for approve may fail if you have approved the token to swapContract before
43    // if you want to enlarge approve amount, you should refer to interface of erc20 token
44    await approveCalling.send({gas: Number(gasLimit)})
45}

8. Estimate gas (optional)

Before actually send the transaction, this is double check (or user experience enhancement measures) to check whether the gas spending is normal.

1const gasLimit = await swapCalling.estimateGas(options)
2console.log('gas limit: ', gasLimit)

9. Send transaction!

Now, we can then send the transaction.

For metamask or other explorer’s wallet provider, you can easily write

1// it is suggested to fill the gas with a number a little greater than estimated "gasLimit".
2await swapCalling.send({...options, gas: Number(gasLimit)})

Otherwise, you could use following code

 1// sign transaction
 2// options is returned from getSwapChainWithExactInputCall
 3const signedTx = await web3.eth.accounts.signTransaction(
 4    {
 5        ...options,
 6        to: swapAddress,
 7        data: swapCalling.encodeABI(),
 8        gas: new BigNumber(Number(gasLimit) * 1.1).toFixed(0, 2),
 9    },
10    privateKey
11)
12// send transaction
13const tx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
14console.log('tx: ', tx);

After sending transaction, we will successfully finish swapping with exact amount of input token (if no revert occurred).