Uniswap V3 交互实战(一)-创建流动性

905 阅读2分钟

网络配置

goerli测试网

使用Infura获取goerli测试网的RPC,想办法搞点水。

使用到的合约

合约名称合约地址
UniswapV3Factory0x1F98431c8aD98523631AE4a59f267346ea31F984
SwapRouter0xE592427A0AEce92De3Edee1F18E0157C05861564
NonfungiblePositionManager0xC36442b4a4522E871399CD717aBDD847Ab11FE88
WETH90xB4FBF271143F4FBf7B91A5ded31805e42b2208d6

创建流动性池

token排序

保证token0 < token1

ETH打包

如果要创建(XXX/ETH)流动性需要将ETH打包成WETH,请使用WETH9合约进行打包,这个WETH9的合约地址可以从NonfungiblePositionManager合约的WETH9参数中得到。

const { ethers, BigNumber : BN } = require('ethers');
const provider = new ethers.providers.JsonRpcProvider(process.env.GOERLI_TEST_RPC);
const wallet = new ethers.Wallet(process.env.DEPLOYER_PK, provider);
const WETH9 = {
    address:'0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
    abi:'',
    decimals:18,
}

//Create Contract for WETH9
const contract = new ethers.Contract(WETH9.address, WETH9.abi, wallet);
(async () =>{
    //eg. you want wrap 1 ether to weth
    const amount = ethers.utils.parseUnits("1", WETH9.decimals);
    await contract.deposit({value:amount});
})()

授权合约消费额度

WETH无需授权消费额度,其他的Token需要检查allowance来确定是否需要授权新的额度

    const contract = new ethers.Contract(token.address, token.abi, wallet);
    const tokenAllowance = await contract.allowance(owner, spender);
    if(tokenAllowance < yourDesiredAmount){
        await contract.approve(spender, yourDesiredAmount);
    }

创建Pool

核心方法

NonfungiblePositionManager.sol > createAndInitializePoolIfNecessary()

传入参数

token0的address、token1的address没有好说的,注意是排序后的token。

fee有几个可供选择500、3000、10000,对应的tickerSpacing分别为10、60、200。

最后一个参数price是token0相对于token1的价格的平方根的Q96.64格式,即

一个token0可以兑换多少个token1,然后将这个数值的平方根乘以2的96次方,代码实现如下

    const bn = require('bignumber.js');
    const {BigNumber, BigNumberish} = require("ethers");

    bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 })
    function encodePriceSqrt(reserve1, reserve0) {
        return BigNumber.from(
            new bn(reserve1.toString())
                .div(reserve0.toString())
                .sqrt()
                .multipliedBy(new bn(2).pow(96))
                .integerValue(3)
                .toString()
        )
    }

    //eg. 
    const sqrtPriceX96 = encodePriceSqrt(`token1's amount`, `token0's amount`);

mint流动性

核心方法

    NonfungiblePositionManager.sol > mint()

传入参数为一个对象

    const param = {
            token0:token0.address,
            token1:token1.address,
            fee:fee,
            tickLower:tickLower,
            tickUpper:tickUpper,
            amount0Desired:token0.amount,
            amount1Desired:token1.amount,
            amount0Min:token0.amount.mul(90).div(100),
            amount1Min:token1.amount.mul(90).div(100),
            recipient:wallet.address,
            deadline:deadline,
            sqrtPriceX96:sqrtPriceX96.toString(),
        };

前3个参数没什么好解释的。

关键是第4、5个,这个tick需要一个专门的方法来处理,理论上,这个tick的计算只需下面的公式即可:

tick=log1.0001pricetick=\log_{1.0001}{price}

但是得出的tick需要被tickerSpacing整除才可以,否则调用会被revert掉,且没有任何有用的报错信息,如下图:

WX20230524-223540@2x.png

因此需要一个方法进行转换:

    function priceToTick(price, tickSpacing) {
        let tick = Math.floor(Math.log(price) / Math.log(1.0001));
        for (let i = 1; i < tickSpacing; i++){
            if((tick - i) % tickSpacing === 0){
                tick = tick - i;
                break;
            }
            if((tick + i) % tickSpacing === 0){
                tick = tick + i;
                break;
            }
        }
        return tick;
    }

至于tickLower和tickUpper只需要增减tickSpacing的整数倍即可

const tickLower = priceToTick(price, tickSpacing) - n*tickSpacing;
const tickUpper = priceToTick(price, tickSpacing) - n*tickSpacing;

然后执行mint方法即可。至此流动创建完成。