张墨轩的技术宅

不忘初心,方得始终

TradestationAPI 授权认证机制

Tradestation API 采用了OAuth2.0认证方式(是目前互联网领域最流行的授权机制,用来给第三方应用授权)

xwiki-docker迁移全过程记录

这里整理下我之前实验室维基xwiki-docker迁移全过程:

UniswapV3简析(五)

在外围合约 v3-periphery\contracts\lens 目录下有两个很重要的用于流动池相关数据查询的合约(目录名称lens意思是lenses,透镜,很应景), 分别是:

TickLens.sol, 此工具合约主要用于查询给定的流动池的所有流动性仓位的流动性大小, 这些信息将用于填充Uniswap链下前端信息网站上展示的流动性深度图

Quoter.sol, 此工具合约主要用于模拟真实交易, 获取实际交易输入输出的token数量, 用户在真实交易前, 链下前端需要预先计算出用户输入token能够预期兑换的输出token数, 但是这个计算工作只有链上的流动池合约自己能做到, 而且流动池合约中的swap函数都是会更改合约状态的external函数, 需要消耗gas费, 那么就需要把这个操作当作view/pure函数来使用, 本合约为了实现这个目的而存在. 备注: 新版本此合约已经升级成最新的QuoterV2版本, 新版本合约可以查询更多信息, 比如gas评估等


TickLens合约

参考代码如下:
contract TickLens is ITickLens {
    /// @inheritdoc ITickLens
    function getPopulatedTicksInWord(address pool, int16 tickBitmapIndex)
        public
        view
        override
        returns (PopulatedTick[] memory populatedTicks)
    {
        // fetch bitmap
        uint256 bitmap = IUniswapV3Pool(pool).tickBitmap(tickBitmapIndex); #池内的tick位图

        // calculate the number of populated ticks
        uint256 numberOfPopulatedTicks;
        for (uint256 i = 0; i < 256; i++) {
            if (bitmap & (1 << i) > 0) numberOfPopulatedTicks++;
        }

        // fetch populated tick data
        int24 tickSpacing = IUniswapV3Pool(pool).tickSpacing();
        populatedTicks = new PopulatedTick[](numberOfPopulatedTicks);
        for (uint256 i = 0; i < 256; i++) {
            if (bitmap & (1 << i) > 0) {
                int24 populatedTick = ((int24(tickBitmapIndex) << 8) + int24(i)) * tickSpacing;
                (uint128 liquidityGross, int128 liquidityNet, , , , , , ) = IUniswapV3Pool(pool).ticks(populatedTick); #查询池内流动性仓位相关tick中包含的元数据信息
                populatedTicks[--numberOfPopulatedTicks] = PopulatedTick({
                    tick: populatedTick, #价格边界
                    liquidityNet: liquidityNet, #净流动性
                    liquidityGross: liquidityGross #流动性和
                });
            }
        }
    }
}


Quoter合约

参考代码如下:
contract Quoter is IQuoter, IUniswapV3SwapCallback, PeripheryImmutableState {
    ...

    /// @inheritdoc IUniswapV3SwapCallback
    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes memory path
    ) external view override {
        require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
        (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
        CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
        (bool isExactInput, uint256 amountToPay, uint256 amountReceived) =
            amount0Delta > 0
                ? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta))
                : (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta));
        if (isExactInput) {
            assembly {
                let ptr := mload(0x40)
                mstore(ptr, amountReceived) #保存计算结果
                revert(ptr, 32) #还原交易
            }
        } else {
            // if the cache has been populated, ensure that the full output amount has been received
            if (amountOutCached != 0) require(amountReceived == amountOutCached);
            assembly {
                let ptr := mload(0x40)
                mstore(ptr, amountToPay) #保存计算结果
                revert(ptr, 32) #还原交易
            }
        }
    }

    /// @dev Parses a revert reason that should contain the numeric quote
    function parseRevertReason(bytes memory reason) private pure returns (uint256) {
        if (reason.length != 32) {
            if (reason.length < 68) revert('Unexpected error');
            assembly {
                reason := add(reason, 0x04)
            }
            revert(abi.decode(reason, (string)));
        }
        return abi.decode(reason, (uint256)); #捕获计算结果
    }

    /// @inheritdoc IQuoter
    function quoteExactInputSingle(
        address tokenIn,
        address tokenOut,
        uint24 fee,
        uint256 amountIn,
        uint160 sqrtPriceLimitX96
    ) public override returns (uint256 amountOut) {
        bool zeroForOne = tokenIn < tokenOut;
        try #进行异常捕获
            getPool(tokenIn, tokenOut, fee).swap( #模拟交易,内部会回调uniswapV3SwapCallback
                address(this), // address(0) might cause issues with some tokens
                zeroForOne,
                amountIn.toInt256(),
                sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : sqrtPriceLimitX96,
                abi.encodePacked(tokenIn, fee, tokenOut)
            )
        {} catch (bytes memory reason) { #异常捕获
            return parseRevertReason(reason); #将计算结果返回给链下前端用户
        }
    }

    ...
}

如代码所示, 基本流程:
1.quoteExactInputSingle中调用swap模拟交易
2.在回调函数uniswapV3SwapCallback中保存计算结果, 并且还原交易
3.在quoteExactInputSingle函数的catch代码块中捕获异常, 调用parseRevertReason函数
4.parseRevertReason函数中解析计算结果

UniswapV3简析(四)

SwapRouter合约是直接与链下前端进行交互的外围合约, 主要对外交易兑换功能, 此合约可以看成用户的交易代理合约, 可以协助用户完成一些相对复杂的兑换操作, 用户<==>SwapRouter合约<==>UniswapV3Pool合约

SwapRouter合约主要封装了四个与交易相关的API接口对外提供服务, 分别是: 

exactInputSingle: 两个token之间进行兑换, 已知输入token的数量, 输出token的数量函数内部会自动进行计算

exactInput两个token之间进行兑换, 已知输入token的数量, 支持复杂路径, 比如ETH->DAI, 可以直接在ETH/DAI流动池兑换, 也可以利用ETH->USDC->DAI这个路径, 通过ETH/USDC, USDC/DAI两个流动池接力完成兑换, 至于采取哪个路径, 由链下前端决定, 链下前端会根据情况计算出最优路径.

exactOutputSingle两个token之间进行兑换, 已知输出token的数量, 输入token的数量函数内部会自动进行计算

exactOutput两个token之间进行兑换, 已知输出token的数量, 支持复杂路径(实际并没有实现)

代码位于contracts\SwapRouter.sol, 参考代码如下:
contract SwapRouter is
    ISwapRouter,
    PeripheryImmutableState,
    PeripheryValidation,
    PeripheryPaymentsWithFee,
    Multicall,
    SelfPermit
{
        ...

    /// @inheritdoc IUniswapV3SwapCallback
    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata _data
    ) external override {
        require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
        SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
        (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
        CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

        (bool isExactInput, uint256 amountToPay) =
            amount0Delta > 0
                ? (tokenIn < tokenOut, uint256(amount0Delta))
                : (tokenOut < tokenIn, uint256(amount1Delta));
        if (isExactInput) {
            pay(tokenIn, data.payer, msg.sender, amountToPay);
        } else {
            // either initiate the next swap or pay
            if (data.path.hasMultiplePools()) {
                data.path = data.path.skipToken();
                exactOutputInternal(amountToPay, msg.sender, 0, data);
            } else {
                amountInCached = amountToPay;
                tokenIn = tokenOut; // swap in/out because exact output swaps are reversed
                pay(tokenIn, data.payer, msg.sender, amountToPay);
            }
        }
    }


    /// @dev Performs a single exact input swap
    function exactInputInternal(
        uint256 amountIn,
        address recipient,
        uint160 sqrtPriceLimitX96,
        SwapCallbackData memory data
    ) private returns (uint256 amountOut) {
        // allow swapping to the router address with address 0
        if (recipient == address(0)) recipient = address(this);

        (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); #路径解码

        bool zeroForOne = tokenIn < tokenOut; #根据地址大小决定池内是token0->token1还是token1->token0

        (int256 amount0, int256 amount1) =
            getPool(tokenIn, tokenOut, fee).swap(
                recipient, #收款地址
                zeroForOne,
                amountIn.toInt256(), #输入token的数量
                sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : sqrtPriceLimitX96, #价格限制,卖设置最低价,买设置最高价
                abi.encode(data)
            );

        return uint256(-(zeroForOne ? amount1 : amount0)); #输出token数量用负数表示
    }


    /// @inheritdoc ISwapRouter
    function exactInputSingle(ExactInputSingleParams calldata params)
        external
        payable
        override
        checkDeadline(params.deadline)
        returns (uint256 amountOut)
    {
        amountOut = exactInputInternal(
            params.amountIn,
            params.recipient,
            params.sqrtPriceLimitX96,
            SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender})
        );
        require(amountOut >= params.amountOutMinimum, 'Too little received');
    }


    /// @inheritdoc ISwapRouter
    function exactInput(ExactInputParams memory params)
        external
        payable
        override
        checkDeadline(params.deadline)
        returns (uint256 amountOut)
    {
        address payer = msg.sender; // 第一个交易对肯定需要用户支付token

        while (true) {
            bool hasMultiplePools = params.path.hasMultiplePools(); #路径中是否包含多个交易对

            // the outputs of prior swaps become the inputs to subsequent ones
            params.amountIn = exactInputInternal(
                params.amountIn,
                hasMultiplePools ? address(this) : params.recipient, // 如果是复杂路径交易,中间token由合约自己先代收
                0,
                SwapCallbackData({
                    path: params.path.getFirstPool(), // only the first pool in the path is necessary
                    payer: payer
                })
            );

            // decide whether to continue or terminate
            if (hasMultiplePools) {
                payer = address(this); // 如果是复杂路径交易,中间token由合约自己先代收,所以也需要合约自己支付到下一个流动池
                params.path = params.path.skipToken();
            } else {
                amountOut = params.amountIn;
                break;
            }
        }

        require(amountOut >= params.amountOutMinimum, 'Too little received');
    }


    /// @dev Performs a single exact output swap
    function exactOutputInternal(
        uint256 amountOut,
        address recipient,
        uint160 sqrtPriceLimitX96,
        SwapCallbackData memory data
    ) private returns (uint256 amountIn) {
        // allow swapping to the router address with address 0
        if (recipient == address(0)) recipient = address(this);

        (address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool(); #路径解码

        bool zeroForOne = tokenIn < tokenOut; #根据地址大小决定池内是token0->token1还是token1->token0

        (int256 amount0Delta, int256 amount1Delta) =
            getPool(tokenIn, tokenOut, fee).swap(
                recipient,
                zeroForOne,
                -amountOut.toInt256(), #负数表示的是输出token数量
                sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : sqrtPriceLimitX96,
                abi.encode(data)
            );

        uint256 amountOutReceived;
        (amountIn, amountOutReceived) = zeroForOne
            ? (uint256(amount0Delta), uint256(-amount1Delta))
            : (uint256(amount1Delta), uint256(-amount0Delta));
        // it's technically possible to not receive the full output amount,
        // so if no price limit has been specified, require this possibility away
        if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);
    }


    /// @inheritdoc ISwapRouter
    function exactOutputSingle(ExactOutputSingleParams calldata params)
        external
        payable
        override
        checkDeadline(params.deadline)
        returns (uint256 amountIn)
    {
        // avoid an SLOAD by using the swap return data
        amountIn = exactOutputInternal(
            params.amountOut,
            params.recipient,
            params.sqrtPriceLimitX96,
            SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender})
        );

        require(amountIn <= params.amountInMaximum, 'Too much requested');
        // has to be reset even though we don't use it in the single hop case
        amountInCached = DEFAULT_AMOUNT_IN_CACHED;
    }


    /// @inheritdoc ISwapRouter
    function exactOutput(ExactOutputParams calldata params)
        external
        payable
        override
        checkDeadline(params.deadline)
        returns (uint256 amountIn)
    {
        // it's okay that the payer is fixed to msg.sender here, as they're only paying for the "final" exact output
        // swap, which happens first, and subsequent swaps are paid for within nested callback frames
        exactOutputInternal(
            params.amountOut,
            params.recipient,
            0,
            SwapCallbackData({path: params.path, payer: msg.sender})
        );

        amountIn = amountInCached;
        require(amountIn <= params.amountInMaximum, 'Too much requested');
        amountInCached = DEFAULT_AMOUNT_IN_CACHED;
    }
}

这里有几个细节要注意一下:

1.当创建流动池的时候就已经确保了池内的token0地址小于token1地址, 所以当用户想用tokenIn兑换tokenOut的时候, 也就是tokenIn=>tokenOut, 那么需要判断它俩地址的大小, 当tokenIn<tokenOut的时候, tokenIn等于池内的token0, tokenOut等于池内的token1, 也就是token0=>token1. tokenIn>tokenOut的时候, tokenIn等于池内的token1, tokenOut等于池内的token0, 也就是token1=>token0

2.对于每一个流动池, 池内的token0是基础货币, token1是计价货币, 池内的价格指的是token0的价格, 以token1计价, 所以用token0兑换token1(token0=>token1), 可以理解为卖出操作, 对于卖出而言, sqrtPriceLimitX96指的是可以接受的最低卖出价格. 而用token1兑换token0(token1=>token0), 可以理解为买入操作, 对于买入而言, sqrtPriceLimitX96指的是可以接受的最高买入价格. 最后需要再次强调的是一个交易对在流动池里面只会以交易对中两个token合约地址大小来确定顺序, 而不会考虑日常的习惯, 比如交易对LINK/USDT, 假如LINK合约地址大于USDT合约地址, 那么它们在流动池里面实际是USDT/LINK, 也就是说token0是USDT, token1是LINK.

3.可以用checkDeadline修改器来实现一笔订单的有效期检查, 代码位于contracts\base\PeripheryValidation.sol, 参考代码如下:
abstract contract PeripheryValidation is BlockTimestamp {
    modifier checkDeadline(uint256 deadline) {
        require(_blockTimestamp() <= deadline, 'Transaction too old'); #_blockTimestamp()内部就是block.timestamp
        _;
    }
}
代码其实很简单, 就是通过检查当前区块时间来判断订单是否过期


前面有提到复杂路径问题, 比如ETH->DAI, 可以直接在ETH/DAI流动池兑换, 也可以利用ETH->USDC->DAI这个路径, 通过ETH/USDC, USDC/DAI两个流动池接力完成兑换, 之所以需要复杂路径一般是出于两方面考虑:

1.简单路径下可能没有对应的流动池可以交易, 比如你想用A兑换B, 可能Uniswap并没有支持A/B这个交易对的流动池, 但是有A/C和B/C, 那么你可以通过这两个流动池组合完成整个兑换动作

2.为了优化成本, 假如A/B这个池流动性不好, 但是A/C和B/C两个池流动性很好, 那么大概率利用这两个池的组合完成的交易比单独用A/B这个池完成的交易费用要更低

关于优化交易成本的问题这里可以稍微展开说说, 其实SwapRouter合约只实现了纵向路径, 比如前面说的ETH->DAI, 可以直接在ETH/DAI流动池兑换, 也可以利用ETH->USDC->DAI这个路径, 这种是属于纵向路径, 其实还可以采用横向路径, 也有机会能节约交易成本, Uniswap V3中同一个交易对是可能会对应多个不同手续费的流动池的, 比如ETH/USDT, 可能就会有一个千三手续费的流动池和一个万五手续费的流动池, 所以你在做一笔大额交易的时候, 如果将订单进行横向拆分, 在两个池分别进行交易, 如果滑点损失小到一定程度, 那这个横向路径的交易可能比单池交易会更优.

Uniswap交易成本可以拆成三部分:
1.链上gas费
2.固定的交易手续费, 比如千三, 万五等
3.滑点, 受流动性大小的影响

不管是横向路径的拆分还是纵向路径的拆分, 亦或者横向纵向相结合, 他们相对于单路径单流动池的交易成本的比较如下表:
路径\成本
gas费
交易手续费
滑点
总成本
纵向路径拆分
上升(代码更多)取决于选择的池子
取决于池子流动性
三者相加
横向路径拆分
上升(代码更多)
取决于选择的池子
取决于池子流动性
三者相加
首先, gas费肯定会上升, 因为代码更多更复杂, 但是如果选择得当, 交易手续费和滑点是可能下降的, 特别是滑点在大额交易的时候比较明显, 这个领域做的最好的项目就是1inch, 以后有时间可以单独拿出来分析.


从前面的代码可以看出, Uniswap V3主要是针对ERC20标准的交易对进行操作, 那么如果交易对中包括以太坊上的原生代币ETH的话, 比如ETH/USDT, 这种情况下系统内部其实会隐式的先把ETH一比一转换成ERC20标准的包装代币WETH, 变成WETH/USDT, 操作完后再将WETH兑换回ETH, 相关代码位于contracts\base\PeripheryPayments.sol, 参考代码如下:
abstract contract PeripheryPayments is IPeripheryPayments, PeripheryImmutableState {
    receive() external payable {
        require(msg.sender == WETH9, 'Not WETH9');
    }

    /// @inheritdoc IPeripheryPayments
    function unwrapWETH9(uint256 amountMinimum, address recipient) external payable override {
        uint256 balanceWETH9 = IWETH9(WETH9).balanceOf(address(this));
        require(balanceWETH9 >= amountMinimum, 'Insufficient WETH9');
        if (balanceWETH9 > 0) {
            IWETH9(WETH9).withdraw(balanceWETH9); #WETH转换成ETH
            TransferHelper.safeTransferETH(recipient, balanceWETH9);
        }
    }

    /// @inheritdoc IPeripheryPayments
    function sweepToken(
        address token,
        uint256 amountMinimum,
        address recipient
    ) external payable override {
        uint256 balanceToken = IERC20(token).balanceOf(address(this));
        require(balanceToken >= amountMinimum, 'Insufficient token');
        if (balanceToken > 0) {
            TransferHelper.safeTransfer(token, recipient, balanceToken);
        }
    }

    /// @inheritdoc IPeripheryPayments
    function refundETH() external payable override {
        if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance);
    }

    /// @param token The token to pay
    /// @param payer The entity that must pay
    /// @param recipient The entity that will receive payment
    /// @param value The amount to pay
    function pay(
        address token,
        address payer,
        address recipient,
        uint256 value
    ) internal {
        if (token == WETH9 && address(this).balance >= value) {
            // pay with WETH9
            IWETH9(WETH9).deposit{value: value}(); // ETH转换成WETH
            IWETH9(WETH9).transfer(recipient, value);
        } else if (payer == address(this)) {
            // pay with tokens already in the contract (for the exact input multihop case)
            TransferHelper.safeTransfer(token, recipient, value);
        } else {
            // pull payment
            TransferHelper.safeTransferFrom(token, payer, recipient, value);
        }
    }
}

其中WETH9合约的参考代码如下:
contract WETH9 {
    string public name     = "Wrapped Ether";
    string public symbol   = "WETH";
    uint8  public decimals = 18;

    event  Approval(address indexed src, address indexed guy, uint wad);
    event  Transfer(address indexed src, address indexed dst, uint wad);
    event  Deposit(address indexed dst, uint wad);
    event  Withdrawal(address indexed src, uint wad);

    mapping (address => uint)                       public  balanceOf;
    mapping (address => mapping (address => uint))  public  allowance;

    function() public payable { #当仅仅接收到ETH转账时此函数默认会被调用
        deposit();
    }

    function deposit() public payable {
        balanceOf[msg.sender] += msg.value; #ETH一比一兑换成WETH
        Deposit(msg.sender, msg.value);
    }

    function withdraw(uint wad) public {
        require(balanceOf[msg.sender] >= wad);
        balanceOf[msg.sender] -= wad; #减少该用户一定量的WETH
        msg.sender.transfer(wad); #将等量的ETH发还给用户
        Withdrawal(msg.sender, wad);
    }

    function totalSupply() public view returns (uint) {
        return this.balance;
    }

    function approve(address guy, uint wad) public returns (bool) {
        allowance[msg.sender][guy] = wad;
        Approval(msg.sender, guy, wad);
        return true;
    }

    function transfer(address dst, uint wad) public returns (bool) {
        return transferFrom(msg.sender, dst, wad);
    }

    function transferFrom(address src, address dst, uint wad)
        public
        returns (bool)
    {
        require(balanceOf[src] >= wad);
        if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) {
            require(allowance[src][msg.sender] >= wad);
            allowance[src][msg.sender] -= wad;
        }
        balanceOf[src] -= wad;
        balanceOf[dst] += wad;
        Transfer(src, dst, wad);
        return true;
    }
}

最后看一下链下前端如何调用交易接口, 这个和之前调用NonfungiblePositionManager合约一样, 也需要通过multicall, 相关代码位于:v3-sdk\src\swapRouter.ts, 参考代码如下:
public static swapCallParameters(
    trades: Trade<Currency, Currency, TradeType> | Trade<Currency, Currency, TradeType>[],
    options: SwapOptions
): MethodParameters {
    ...
    const calldatas: string[] = []
    ...
    // encode permit if necessary
    if (options.inputTokenPermit) {
        invariant(sampleTrade.inputAmount.currency.isToken, 'NON_TOKEN_PERMIT')
        calldatas.push(SelfPermit.encodePermit(sampleTrade.inputAmount.currency, options.inputTokenPermit))
    }
    ...
    for (const trade of trades) {
        for (const { route, inputAmount, outputAmount } of trade.swaps) {
            ...
            // flag for whether the trade is single hop or not
            const singleHop = route.pools.length === 1
            if (singleHop) {
                if (trade.tradeType === TradeType.EXACT_INPUT) {
                    ...
                    calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactInputSingle', [exactInputSingleParams]))
                } else {
                    ...
                    calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactOutputSingle', [exactOutputSingleParams]))
                }
            } else {
                ...
                if (trade.tradeType === TradeType.EXACT_INPUT) {
                    ...
                    calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactInput', [exactInputParams]))
                } else {
                    ...
                    calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactOutput', [exactOutputParams]))
                }
            }
        }
    }

    // unwrap
    if (routerMustCustody) {
        if (!!options.fee) {
            if (outputIsNative) {
                calldatas.push(Payments.encodeUnwrapWETH9(totalAmountOut.quotient, recipient, options.fee))
            } else {
              calldatas.push(
                  Payments.encodeSweepToken(
                  sampleTrade.outputAmount.currency.wrapped,
                  totalAmountOut.quotient,
                  recipient,
                  options.fee))
            }
        } else {
            calldatas.push(Payments.encodeUnwrapWETH9(totalAmountOut.quotient, recipient))
        }
    }

    // refund
    if (mustRefund) {
        calldatas.push(Payments.encodeRefundETH())
    }

    return {
        calldata: Multicall.encodeMulticall(calldatas),
        value: toHex(totalValue.quotient)
    }
}
从上面代码可以看到, 其实就是根据业务逻辑将 [token授权转账权限, 交易兑换, WETH/ETH转换] 等操作, 整体打包成一次multicall调用, 要么全部成功, 要么全部失败

UniswapV3简析(三)

NonfungiblePositionManager合约是直接与链下前端进行交互的外围合约, 主要对外提供比如创建流动池, 添加流动性, 删除流动性等API接口, 同时也会为每个流动性仓位生成一个唯一的NFT给到用户, 可以说是Uniswap协议中最为重要的一个管理器, 这里有一个关键点前文已经说过, 这里需要再次强调一下, 就是NonfungiblePositionManager合约可以同时管理多个流动池Pool(每个流动池包含一个交易对), 其由(token0,token1,fee)唯一标识, 而每个流动池又是一个流动性聚合器其同时包含多个流动性仓位, 而每个流动性仓位又可以由(owner,tickLower,tickUpper)唯一进行标识, 如下图: 
NonfungiblePositionManager合约对外提供一些重要的API:

createAndInitializePoolIfNecessary,  部署一个新的流动池合约

selfPermitAllowed/selfPermit, 授权合约有转账相应token的权限

mint, 给指定流动池添加一个新的流动性仓位, 并且生成一个代表此仓位的NFT给到用户

increaseLiquidity, 给指定流动池某个已经存在的流动性仓位增加流动性

decreaseLiquidity, 指定流动池某个已经存在的流动性仓位减少流动性

collect, 提取闲置token及其手续费

multicall, 将多个合约调用打包进一次合约调用中, 这个其实是为了达到类似数据库事务(transaction)的效果, 一个事务应该满足:
1.原子性(Atomicity): 事务中的全部操作在数据库中是不可分割的, 要么全部完成, 要么全部不执行
2.一致性(Consistency): 几个并行执行的事务, 其执行结果必须与按某一顺序串行执行的结果相一致
3.隔离性(Isolation): 事务的执行不受其他事务的干扰, 事务执行的中间结果对其他事务必须是透明的
4.持久性(Durability): 对于任意已提交事务, 系统必须保证该事务对数据库的改变不被丢失, 即使数据库出现故障


部署新流动池

createAndInitializePoolIfNecessary函数用于部署一个新的流动池合约, 其代码位于contracts\base\PoolInitializer.sol中, 参考代码如下:

function createAndInitializePoolIfNecessary(
    address token0,
    address token1,
    uint24 fee,
    uint160 sqrtPriceX96
) external payable override returns (address pool) {
    require(token0 < token1);
    pool = IUniswapV3Factory(factory).getPool(token0, token1, fee); #查询流动池是否存在
    if (pool == address(0)) { #流动池不存在
        pool = IUniswapV3Factory(factory).createPool(token0, token1, fee); #部署新的流动池合约
        IUniswapV3Pool(pool).initialize(sqrtPriceX96); #设置流动池初始价格
    } else { #流动池已经存在
        (uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0(); #获取流动池当前价格
        if (sqrtPriceX96Existing == 0) { #价格不存在就设置它
            IUniswapV3Pool(pool).initialize(sqrtPriceX96);
        }
    }
}

基本逻辑挺简单, 就是利用(token0, token1, fee)三个参数去工厂合约查询相应的流动池是否已经存在, 如果不存在就调用工厂合约的createPool函数在链上部署一个新的流动池合约, 然后设置这个池子的初始价格.


给指定流动池添加一个新的流动性仓位

mint函数给指定流动池添加一个新的流动性仓位, 并且生成一个代表此仓位的NFT给到用户, 其代码位于contracts\NonfungiblePositionManager.sol, 参考代码如下:

function mint(MintParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (
        uint256 tokenId,
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    )
{
    IUniswapV3Pool pool;
    (liquidity, amount0, amount1, pool) = addLiquidity( #此函数内部真正添加流动性,并完成token的转账充值
        AddLiquidityParams({
            token0: params.token0, #唯一标识一个流动池
            token1: params.token1, #唯一标识一个流动池
            fee: params.fee, #唯一标识一个流动池
            recipient: address(this), #唯一标识该池中的一个流动性仓位
            tickLower: params.tickLower, #唯一标识该池中的一个流动性仓位
            tickUpper: params.tickUpper, #唯一标识该池中的一个流动性仓位
            amount0Desired: params.amount0Desired, #要充值的token0数量
            amount1Desired: params.amount1Desired, #要充值的token1数量
            amount0Min: params.amount0Min,
            amount1Min: params.amount1Min
        })
    );

    _mint(params.recipient, (tokenId = _nextId++)); #生成一个代表流动性仓位的NFT给到用户

    bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);

    // idempotent set
    uint80 poolId =
        cachePoolKey(
            address(pool),
            PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee})
        );

    _positions[tokenId] = Position({ #将NFT与对应的流动性仓位进行关联
        nonce: 0,
        operator: address(0),
        poolId: poolId, #该NFT所代表的流动性仓位属于哪个流动池
        tickLower: params.tickLower, #该NFT所代表的流动性仓位做市区间最低点位
        tickUpper: params.tickUpper, #该NFT所代表的流动性仓位做市区间最高点位
        liquidity: liquidity, #该NFT所代表的流动性仓位的流动性大小, 也就是L值
        feeGrowthInside0LastX128: feeGrowthInside0LastX128, #计算手续费相关
        feeGrowthInside1LastX128: feeGrowthInside1LastX128, #计算手续费相关
        tokensOwed0: 0,
        tokensOwed1: 0
    });

    emit IncreaseLiquidity(tokenId, liquidity, amount0, amount1);
}

代码整体流程很清晰, 这里关键就是addLiquidity函数, 其代码位于contracts\base\LiquidityManagement.sol, 参考代码如下:
function addLiquidity(AddLiquidityParams memory params)
    internal
    returns (
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1,
        IUniswapV3Pool pool
    )
{
    PoolAddress.PoolKey memory poolKey =
        PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee});

    pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); #通过(token0,token1,fee)定位流动池合约地址

    // compute the liquidity amount
    {
        (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); #流动池当前价格
        uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); #做市区间最低价
        uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); #做市区间最高价
        #这里其实就是前文提过关于L,P,x,y四者的相互运算
        liquidity = LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96,
            sqrtRatioAX96,
            sqrtRatioBX96,
            params.amount0Desired,
            params.amount1Desired
        );
    }
    #调用流动池的mint函数完成流动性添加,此函数内部会回调我们的uniswapV3MintCallback函数完成充值,代码在下面
    (amount0, amount1) = pool.mint(
        params.recipient,
        params.tickLower,
        params.tickUpper,
        liquidity,
        abi.encode(MintCallbackData({poolKey: poolKey, payer: msg.sender}))
    );

    require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');
}


function uniswapV3MintCallback(
    uint256 amount0Owed, #要充值的token0数量
    uint256 amount1Owed, #要充值的token1数量
    bytes calldata data
) external override {
    MintCallbackData memory decoded = abi.decode(data, (MintCallbackData));
    CallbackValidation.verifyCallback(factory, decoded.poolKey);
    #将token转账给流动池合约地址, 完成充值
    if (amount0Owed > 0) pay(decoded.poolKey.token0, decoded.payer, msg.sender, amount0Owed);
    if (amount1Owed > 0) pay(decoded.poolKey.token1, decoded.payer, msg.sender, amount1Owed);
}


给指定流动池某个已经存在的流动性仓位增加流动性

increaseLiquidity函数用于给指定流动池某个已经存在的流动性仓位增加流动性, 其代码位于contracts\NonfungiblePositionManager.sol, 参考代码如下:
function increaseLiquidity(IncreaseLiquidityParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    )
{
    Position storage position = _positions[params.tokenId]; #根据传入的 NFT id 获取对应的流动性仓位信息

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; #获取该流动性仓位所属的流动池

    IUniswapV3Pool pool;
    (liquidity, amount0, amount1, pool) = addLiquidity( #向该流动池添加流动性, 不在赘述
        AddLiquidityParams({
            token0: poolKey.token0,
            token1: poolKey.token1,
            fee: poolKey.fee,
            tickLower: position.tickLower,
            tickUpper: position.tickUpper,
            amount0Desired: params.amount0Desired,
            amount1Desired: params.amount1Desired,
            amount0Min: params.amount0Min,
            amount1Min: params.amount1Min,
            recipient: address(this)
        })
    );

    bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper);

    // this is now updated to the current transaction
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); #获取该流动池中对应的这个流动性仓位

    position.tokensOwed0 += uint128( #更新这个流动性仓位之前获取的做市手续费
        FullMath.mulDiv(
            feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
            position.liquidity,
            FixedPoint128.Q128
        )
    );
    position.tokensOwed1 += uint128( #更新这个流动性仓位之前获取的做市手续费
        FullMath.mulDiv(
            feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
            position.liquidity,
            FixedPoint128.Q128
        )
    );

    position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; #更新手续费计算相关
    position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; #更新手续费计算相关
    position.liquidity += liquidity; #更新L值

    emit IncreaseLiquidity(params.tokenId, liquidity, amount0, amount1);
}


给指定流动池某个已经存在的流动性仓位减少流动性

decreaseLiquidity函数用于指定流动池某个已经存在的流动性仓位减少流动性, 其代码位于contracts\NonfungiblePositionManager.sol, 参考代码如下:
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
    external
    payable
    override
    isAuthorizedForToken(params.tokenId)
    checkDeadline(params.deadline)
    returns (uint256 amount0, uint256 amount1)
{
    require(params.liquidity > 0);
    Position storage position = _positions[params.tokenId]; #根据传入的 NFT id 获取对应的流动性仓位信息

    uint128 positionLiquidity = position.liquidity;
    require(positionLiquidity >= params.liquidity);

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; #获取该流动性仓位所属的流动池key
    IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); #获取该流动性仓位所属的流动池合约地址
    (amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity); #调用该流动池合约的burn函数,真正完成减少流动性

    require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');

    bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper); #获取该流动性仓位的key
    // this is now updated to the current transaction
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); #从指定流动池中获取该流动性仓位的手续费相关信息

    position.tokensOwed0 +=
        uint128(amount0) +
        uint128(
            FullMath.mulDiv(
                feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
                positionLiquidity,
                FixedPoint128.Q128
            )
        ); #减少流动性后,会空闲出多余token,记录之,同时也会更新之前的做市手续费收入
    position.tokensOwed1 +=
        uint128(amount1) +
        uint128(
            FullMath.mulDiv(
                feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                positionLiquidity,
                FixedPoint128.Q128
            )
        ); #减少流动性后,会空闲出多余token,记录之,同时也会更新之前的做市手续费收入

    position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; #更新手续费计算相关
    position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; #更新手续费计算相关
    // subtraction is safe because we checked positionLiquidity is gte params.liquidity
    position.liquidity = positionLiquidity - params.liquidity; #更新L值

    emit DecreaseLiquidity(params.tokenId, params.liquidity, amount0, amount1);
}


提取闲置token及其手续费

collect函数用于提取闲置token及其手续费, 其代码位于contracts\NonfungiblePositionManager.sol, 参考代码如下:
function collect(CollectParams calldata params)
    external
    payable
    override
    isAuthorizedForToken(params.tokenId)
    returns (uint256 amount0, uint256 amount1)
{
    require(params.amount0Max > 0 || params.amount1Max > 0);
    // allow collecting to the nft position manager address with address 0
    address recipient = params.recipient == address(0) ? address(this) : params.recipient;

    Position storage position = _positions[params.tokenId]; #根据传入的 NFT id 获取对应的流动性仓位信息

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; #根据流动性仓位信息找所属的流动池

    IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); #获取该流动池合约地址

    (uint128 tokensOwed0, uint128 tokensOwed1) = (position.tokensOwed0, position.tokensOwed1); #真正可提取的token数量

    // trigger an update of the position fees owed and fee growth snapshots if it has any liquidity
    if (position.liquidity > 0) {
        pool.burn(position.tickLower, position.tickUpper, 0); #这里并不是减少流动性,而仅仅只是为了更新手续费
        (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) =
            pool.positions(PositionKey.compute(address(this), position.tickLower, position.tickUpper));

        tokensOwed0 += uint128(
            FullMath.mulDiv(
                feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
                position.liquidity,
                FixedPoint128.Q128
            )
        ); #把最新的可提取的手续费收入也累加进来
        tokensOwed1 += uint128(
            FullMath.mulDiv(
                feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                position.liquidity,
                FixedPoint128.Q128
            )
        ); #把最新的可提取的手续费收入也累加进来

        position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
        position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
    }

    // compute the arguments to give to the pool#collect method
    (uint128 amount0Collect, uint128 amount1Collect) =
        (
            params.amount0Max > tokensOwed0 ? tokensOwed0 : params.amount0Max,
            params.amount1Max > tokensOwed1 ? tokensOwed1 : params.amount1Max
        );

    // the actual amounts collected are returned
    (amount0, amount1) = pool.collect( #调用流动池合约的collect函数,真正完成转账提取token的操作
        recipient, #用户收钱地址
        position.tickLower, #用于标识对应的流动性仓位
        position.tickUpper, #用于标识对应的流动性仓位
        amount0Collect, #期望提取的token数量
        amount1Collect  #期望提取的token数量
    );

    // sometimes there will be a few less wei than expected due to rounding down in core, but we just subtract the full amount expected
    // instead of the actual amount so we can burn the token
    (position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect);

    emit Collect(params.tokenId, recipient, amount0Collect, amount1Collect);
}


将多个合约调用打包进一次合约调用

multicall, 将多个合约调用打包进一次合约调用中, 这是事务性的关键操作, 其代码位于contracts\base\Multicall.sol, 参考代码如下:
function multicall(bytes[] calldata data) external payable override returns (bytes[] memory results) {
    results = new bytes[](data.length);
    for (uint256 i = 0; i < data.length; i++) {
        (bool success, bytes memory result) = address(this).delegatecall(data[i]);
        if (!success) {
            // Next 5 lines from https://ethereum.stackexchange.com/a/83577
            if (result.length < 68) revert();
            assembly {
                result := add(result, 0x04)
            }
            revert(abi.decode(result, (string)));
        }
        results[i] = result;
    }
}

可以看到逻辑其实很简单, 就是在一个for循环中依次调用之前打包的各个合约调用, 这里有一个非常关键的技术要点就是delegatecall, delegatecall的作用是当用户A通过合约B来delegatecall合约C的时候, 执行的是合约C的函数, 但是语境仍是合约B的: msg.sender是A的地址, 并且如果函数改变一些状态变量, 产生的效果会作用于合约B的变量上, 如下图:

这里可以参考链下前端代码, 其代码位于v3-sdk\src\nonfungiblePositionManager.ts, 比如addCallParameters函数, 参考代码如下:
public static addCallParameters(position: Position, options: AddLiquidityOptions): MethodParameters {
    ...

    // create pool if needed
    if (isMint(options) && options.createPool) {
      calldatas.push(this.encodeCreate(position.pool)) #打包createAndInitializePoolIfNecessary调用
    }

    // permits if necessary
    if (options.token0Permit) {
      calldatas.push(SelfPermit.encodePermit(position.pool.token0, options.token0Permit)) #打包selfPermitAllowed/selfPermit调用
    }
    if (options.token1Permit) {
      calldatas.push(SelfPermit.encodePermit(position.pool.token1, options.token1Permit)) #打包selfPermitAllowed/selfPermit调用
    }

    // mint
    if (isMint(options)) {
      const recipient: string = validateAndParseAddress(options.recipient)

      calldatas.push( #打包mint调用
        NonfungiblePositionManager.INTERFACE.encodeFunctionData('mint', [
            ...
        ])
      )
    } else {
      // increase
      calldatas.push( #打包increaseLiquidity调用
        NonfungiblePositionManager.INTERFACE.encodeFunctionData('increaseLiquidity', [
            ...
        ])
      )
    }

    ...

    return {
      calldata: Multicall.encodeMulticall(calldatas), #最后将上面所有调用都打包成一个multicall调用
      value
    }
}

从上面代码可以看到, 其实就是将 [创建流动池, token授权转账权限, 添加流动性] 等操作, 整体打包成一次multicall调用, 要么全部成功, 要么全部失败

UniswapV3简析(二)

UniswapV3Pool流动池合约实现了Uniswap协议最核心的功能, 它并不与链下直接进行交互, 而只是与外围合约比如NonfungiblePositionManager, SwapRouter进行交互.

Uniswap本质上是由N多个独立的流动池构成, 每个流动池由(token0,token1,fee)三个参数唯一进行标识, 通过这三个参数可以唯一检索出对应的Pool地址, 参考代码如下:

PoolAddress.PoolKey memory poolKey = PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee});
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));

这里有两个细节需要强调:

第一, 在部署Pool合约的时候会通过 keccak256(abi.encode(token0, token1, fee) 将 token0, token1, fee 作为输入, 得到一个哈希值, 并将其作为 salt 来创建合约. 因为指定了 salt, solidity 会使用 EVM 的 CREATE2 指令来创建合约, 使用 CREATE2 指令的好处是, 只要合约的 bytecode 及 salt 不变, 那么创建出来的地址也将不变. 这样我们就可以直接在链下计算出已经创建的流动池的地址, 而不需要进行链上查询.

第二, 在部署Pool合约的时候会先将token0和token1按其合约地址按从小到大进行排序, 换句话说流动池中的token0合约地址肯定小于token1合约地址.

之前说过, 每一个流动池实际上是一个流动性聚合器, 可以同时添加多个流动性仓位, 每个流动性仓位由(owner,tickLower,tickUpper)三个参数唯一标识, 这三个参数的含义分别为:

owner: 流动性仓位的拥有者地址(实际上就是NonfungiblePositionManager合约)
tickLower: 流动性仓位做市区间最低价的tick索引
tickUpper流动性仓位做市区间最高价的tick索引

我们可以通过上面三个参数唯一检索出对应的流动性仓位, 参考代码如下:

library Position {
    /// @notice Returns the Info struct of a position, given an owner and position boundaries
    /// @param self The mapping containing all user positions
    /// @param owner The address of the position owner
    /// @param tickLower The lower tick boundary of the position
    /// @param tickUpper The upper tick boundary of the position
    /// @return position The position info struct of the given owners' position
    function get(
        mapping(bytes32 => Info) storage self,
        address owner,
        int24 tickLower,
        int24 tickUpper
    ) internal view returns (Position.Info storage position) {
        position = self[keccak256(abi.encodePacked(owner, tickLower, tickUpper))];
    }

    ......

}

注意, 这里的仓位拥有者都是NonfungiblePositionManager合约, 而不是实际的用户地址, 只所以这样做, 一是可以让不同用户添加的价格区间一样的流动性合并管理, 二是为了让流动性与实际用户进行解耦, 这样在外围合约NonfungiblePositionManager中可以独立管理流动性, 每个流动性仓位分配一个NFT给到用户, 由NFT来代表流动性仓位的所有权, 用户可以通过转移NFT来进行流动性仓位所有权的转移

每一个流动性仓位由以下数据结构所表示:

library Position {
    // info stored for each user's position
    struct Info {
        // the amount of liquidity owned by this position
        uint128 liquidity;
        // fee growth per unit of liquidity as of the last update to liquidity or fees owed
        uint256 feeGrowthInside0LastX128;
        uint256 feeGrowthInside1LastX128;
        // the fees owed to the position owner in token0/token1
        uint128 tokensOwed0;
        uint128 tokensOwed1;
    }

    ......

}

因为每个流动池都包含多个流动性仓位, 交易过程中会经常跨越多个流动性仓位的价格边界, 为了算法需要, 每个流动性仓位都在价格区间的两端分别对应一个tick结构, 里面包含了这个价格点位上的若干元数据, 其数据结构如下:

library Tick {

    // info stored for each initialized individual tick
    struct Info {
        // the total position liquidity that references this tick
        uint128 liquidityGross;
        // amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left),
        int128 liquidityNet;
        // fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick)
        // only has relative meaning, not absolute — the value depends on when the tick is initialized
        uint256 feeGrowthOutside0X128;
        uint256 feeGrowthOutside1X128;
        // the cumulative tick value on the other side of the tick
        int56 tickCumulativeOutside;
        // the seconds per unit of liquidity on the _other_ side of this tick (relative to the current tick)
        // only has relative meaning, not absolute — the value depends on when the tick is initialized
        uint160 secondsPerLiquidityOutsideX128;
        // the seconds spent on the other side of the tick (relative to the current tick)
        // only has relative meaning, not absolute — the value depends on when the tick is initialized
        uint32 secondsOutside;
        // true iff the tick is initialized, i.e. the value is exactly equivalent to the expression liquidityGross != 0
        // these 8 bits are set to prevent fresh sstores when crossing newly initialized ticks
        bool initialized;
    }

    ......

}

这里简单总结一下, 每一个流动池包含一个交易对, 其由(token0,token1,fee)唯一标识, 而每个流动池又是一个流动性聚合器其同时包含多个流动性仓位, 而每个流动性仓位又可以由(owner,tickLower,tickUpper)唯一进行标识, 最后每个流动性仓位做市价格区间一高一低两个价格点位上都分别对应一个tick数据结构用于包含价格元数据, 整体结构如下图所示:

每一个流动池内部都有一个Slot0结构, 里面保存了当前价格:

struct Slot0 {
        // the current price
        uint160 sqrtPriceX96;
        // the current tick
        int24 tick;
                ......
    }


添加流动性

function mint(
        address recipient,
        int24 tickLower,
        int24 tickUpper,
        uint128 amount,
        bytes calldata data
    ) external override lock returns (uint256 amount0, uint256 amount1)

参数说明:
recipient - 流动性仓位的拥有者地址
tickLower - 流动性仓位做市价格区间最低点位
tickUpper - 流动性仓位做市价格区间最高点位
amount - 要添加的流动性大小, 也就是L的值
data - 回调函数参数

此函数会调用调用内部函数_modifyPosition, 然后_modifyPosition会调用_updatePosition, 在 _updatePosition函数中会更新position, 及其对应的tick等数据结构, 然后_modifyPosition会依据做市价格区间及其流动L的大小计算需要充值的token0,token1的数量, 假设当前流动池价格为Pc, 用户添加流动性仓位的价格区间的 [Pa, Pb] (Pa < Pb), 实际会分为三种情况:

1.做市价格区间大于当前实际价格, 只能添加x token, 如下图:

流动性L的计算公式为:

2.做市价格区间小于当前实际价格, 只能添加y token, 如下图:

流动性L的计算公式为:

3.做市价格区间包含当前实际价格, 按一定比例分别添加x token和y token, 如下图:

流动性L的计算公式为:

具体的代码实现在contracts\libraries\SqrtPriceMath.sol中, 参考代码:
amount0 = SqrtPriceMath.getAmount0Delta(
                    _slot0.sqrtPriceX96,
                    TickMath.getSqrtRatioAtTick(params.tickUpper),
                    params.liquidityDelta
                );
amount1 = SqrtPriceMath.getAmount1Delta(
                    TickMath.getSqrtRatioAtTick(params.tickLower),
                    _slot0.sqrtPriceX96,
                    params.liquidityDelta
                );

当计算好实际要充值的x token数量与y token数量以后, 就会回调 NonfungiblePositionManager合约的uniswapV3MintCallback函数, 在这个函数里面完成真正的转账充值工作, 然后会检查是否成功收到转账, 如果出现问题系统会进行回滚, 参考代码如下:

if (amount0 > 0) balance0Before = balance0();
if (amount1 > 0) balance1Before = balance1();
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);
if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');
if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');

大致流程如下图所示:
1.用户发起添加流动性的操作
2.NonfungiblePositionManager合约调用UniswapV3Pool合约的mint函数
3.UniswapV3Pool合约在mint函数中回调NonfungiblePositionManager合约的uniswapV3MintCallback函数
4.在uniswapV3MintCallback函数中会分别调用池中两个代币对应的ERC20代币合约的transferFrom函数将用户的token转账给Pool


减少流动性

function burn(
        int24 tickLower,
        int24 tickUpper,
        uint128 amount
    ) external override lock returns (uint256 amount0, uint256 amount1)

参数说明:
tickLower - 做市价格区间最低点位, 用于检索对应流动性仓位
tickUpper - 做市价格区间最高点位, 用于检索对应流动性仓位
amount - 要减少的流动性数量, 也就是L值

此函数逻辑整体较为简单, 内部调用_modifyPosition修改对应流动性仓位的相关数据, 它通过(owner,tickLower,tickUpper)三个参数唯一检索出需要修改的流动性仓位. 因为减少了流动性, 所以必然会空闲出相应数量的token, 程序会把空闲出来的token数量保存到对应仓位的position.tokensOwed0,position.tokensOwed1成员变量中, 参考代码如下:

if (amount0 > 0 || amount1 > 0) {
    (position.tokensOwed0, position.tokensOwed1) = (
        position.tokensOwed0 + uint128(amount0),
        position.tokensOwed1 + uint128(amount1)
    );
}

需要强调的是, 这里仅仅只是记录空闲出来的token数量, 而并不会把这些token返还给客户.


提取闲置token及其手续费

function collect(
        address recipient,
        int24 tickLower,
        int24 tickUpper,
        uint128 amount0Requested,
        uint128 amount1Requested
    ) external override lock returns (uint128 amount0, uint128 amount1)

参数说明:
recipient - 接收返还的用户地址
tickLower - 做市价格区间最低点位, 用于检索对应流动性仓位
tickUpper - 做市价格区间最高点位, 用于检索对应流动性仓位
amount0Requested - 想要提取的token0数量
amount1Requested - 想要提取token1数量

当用户进行减少流动性的操作后, 空闲出来的token数量会被保存到对应仓位的position.tokensOwed0,position.tokensOwed1成员变量中, 另外就是用户充当LP赚取的手续费也会保存在这俩成员变量中, 本函数会真正将相应数量的token返还给用户, 代码很少, 一看便知, 参考代码如下:

Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper); #通过(owner,tickLower,tickUpper)找到对应流动性仓位
amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested; #获取实际可提取数量
amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested; #获取实际可提取数量
if (amount0 > 0) {
    position.tokensOwed0 -= amount0; #更新数量
    TransferHelper.safeTransfer(token0, recipient, amount0); #真正转账
}
if (amount1 > 0) {
    position.tokensOwed1 -= amount1; #更新数量
    TransferHelper.safeTransfer(token1, recipient, amount1); #真正转账
}


交易兑换

function swap(
        address recipient,
        bool zeroForOne,
        int256 amountSpecified,
        uint160 sqrtPriceLimitX96,
        bytes calldata data
    ) external override noDelegateCall returns (int256 amount0, int256 amount1)

参数说明:

recipient - 接收兑换输出的地址
zeroForOne - 兑换方向, 当true时兑换方向为:token0->token1, 当false时兑换方向为:token1到token0
amountSpecified - 兑换数量, 当数值为正数代表是输入token的数量, 当数值为负数代表想达到的输出token数量
sqrtPriceLimitX96 - 价格限制, 根据方向最终价格不能大于或者小于该值
data - 回调函数参数

本函数提供token交易兑换功能, 算是较为复杂的部分, 因为交易过程中可能会跨越多个流动性仓位, 算法有一定的复杂性, 具体细节网上存在大量说明, 就不在赘述, 基本逻辑就是:
1.已知输入token的数量, 计算出可以输出多少数量的token, 及其兑换结束后价格移动到什么点位
2.已知输出token的数量, 计算出需要多少数量的输入token, 及其兑换结束后价格移动到什么点位

分为四种情况处理:

(zeroForOne:true, amountSpecified:正数) ==>> 我用amountSpecified数量的token0, 最多可以兑换多少数量的token1?
(zeroForOne:true, amountSpecified:负数) ==>> 我需要兑换abs(amountSpecified)数量的token1, 最少需要准备多少数量的token0?
(zeroForOne:false, amountSpecified:正数) ==>> 我用amountSpecified数量的token1, 最多可以兑换多少数量的token0?
(zeroForOne:falseamountSpecified:负数) ==>> 兑换abs(amountSpecified)数量的token0, 最少需要准备多少数量的token1?

真正在token交易过程中, 可能会涉及路径选择问题, 比如ETH->DAI, 可以直接在ETH/DAI流动池兑换, 也可以利用ETH->USDC->DAI这个路径, 通过ETH/USDC, USDC/DAI两个流动池接力完成兑换
至于采取哪个路径, 由链下前端决定, 链下前端会根据情况计算出最优路径, 关于程序中路径的编码如下图所示:

当最终计算好要兑换的输入输出token数量以后, swap函数会首先将用户需求的token发送给用户, 然后会回调位于SwapRouter合约中的回调函数uniswapV3SwapCallback, 在uniswapV3SwapCallback函数中最终会用ERC20合约的transferFrom方法将用户的token转帐到流动池合约中, 至此完成兑换过程.

swap函数的最后会首先将token转账给用户地址, 然后回调uniswapV3SwapCallback函数, 参考代码如下: 
if (zeroForOne) {
    if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));
    uint256 balance0Before = balance0();
    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
    require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA');
} else {
    if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0));
    uint256 balance1Before = balance1();
    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
    require(balance1Before.add(uint256(amount1)) <= balance1(), 'IIA');
}

然后存在于SwapRouter合约中的回调函数uniswapV3SwapCallback会将用户的token转帐到流动池合约中, 最终完成兑换过程, 参考代码如下:
function uniswapV3SwapCallback(
    int256 amount0Delta,
    int256 amount1Delta,
    bytes calldata _data
) external override {
    require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
    SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
    (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
    CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
    (bool isExactInput, uint256 amountToPay) =
        amount0Delta > 0
            ? (tokenIn < tokenOut, uint256(amount0Delta))
            : (tokenOut < tokenIn, uint256(amount1Delta));
    if (isExactInput) {
        pay(tokenIn, data.payer, msg.sender, amountToPay);
    } else {
        // either initiate the next swap or pay
        if (data.path.hasMultiplePools()) {
            data.path = data.path.skipToken();
            exactOutputInternal(amountToPay, msg.sender, 0, data);
        } else {
            amountInCached = amountToPay;
            tokenIn = tokenOut; // swap in/out because exact output swaps are reversed
            pay(tokenIn, data.payer, msg.sender, amountToPay);
        }
    }
}

因为采用了回调函数, 所以这里会引出一个非常有意思的概念就是闪电交易(flash swap), 看代码流程就会发现, 流动池合约首先会将用户需求的token发送给用户, 然后再调用回调函数, 流动池合约期望在回调函数中把用户相应token转账回流动池合约来完成一次兑换, 那么这里就会有一个很有意思的现象, 在回调函数将相应token转账回流动池合约之前那一小段时间, 是不是相当于我们向流动池借用了相应token!!!, 如果我们利用自己开发的合约实现此回调函数, 就可以充分利用区块链的特性实现一些非常有意思的功能, 利用这个思路还可以实现闪电贷(flash loan).

最后总结下兑换流程, 如下图所示(以token0兑换token1为例):

1.用户发起兑换交易
2.SwapRouter合约调用UniswapV3Pool合约的swap函数
3.在swap函数中会调用token1合约的transfer方法向用户转账相应数量的token1
4.转账完成后会回调存在于SwapRouter合约中的uniswapV3SwapCallback函数
5.uniswapV3SwapCallback函数会利用token0合约的transferFrom方法将用户相应数量的token0转帐给流动池合约

至此用户在SwapRouter合约的帮助下完成交易兑换过程 (用户EOA地址 <<==>> 流动池合约地址)


闪电贷

function flash(
        address recipient,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external override lock noDelegateCall

参数说明:

recipient - 想借token的用户地址,比如可以是用户自己开发的合约的地址
amount0 - 用户想借入的token0数量
amount1 - 用户想借入的token1数量
data - 回调函数参数

本函数实现了闪电贷(flash loan), 基本原理前文已经有所说明, 基本的借贷要素如下:
借入方: 用户(需要自己开发智能合约)
借出方: 某流动池
借贷标的: 该流动池所拥有的两种token

代码不多, 这里直接讲重点, 参考代码如下: 
function flash(
    address recipient,
    uint256 amount0,
    uint256 amount1,
    bytes calldata data
) external override lock noDelegateCall {
    ...

    uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6); #用户需要支付的借贷手续费
    uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6); #用户需要支付的借贷手续费
    uint256 balance0Before = balance0(); #借贷前的金额
    uint256 balance1Before = balance1(); #借贷前的金额

    if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0); #借出token给用户
    if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1); #借出token给用户

    IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data); #用户应该自己开发合约并实现此回调函数,注意参数是需要支付的手续费数量

    uint256 balance0After = balance0(); #用户已经连本带利归还后的余额
    uint256 balance1After = balance1(); #用户已经连本带利归还后的余额

    require(balance0Before.add(fee0) <= balance0After, 'F0'); #检查用户是不是已经连本带利足额还款, 没还就回滚整个交易
    require(balance1Before.add(fee1) <= balance1After, 'F1'); #检查用户是不是已经连本带利足额还款, 没还就回滚整个交易

    ...
}

简要总结下流程:
1.用户需要自己开发智能合约并按接口规范实现相应回调函数, 并和相应流动池建立连接, 然后调用流动池合约的flash函数
2.flash函数中会将用户需要的token转账到用户指定的地址, 然后回调用户合约的回调函数
3.用户在自己的回调函数中按需完成自己的业务逻辑, 然后连本带利将相应token返还给流动池
4.流动池合约检查用户是不是已经连本带利足额还款, 没还就回滚整个交易

闪电贷这种创新让资金借贷效率产生了一个巨大的飞跃, 可以说是DeFi特有的精髓.

UniswapV3简析(一)


Uniswap V3 是Uniswap V2的升级版, 其最大的创新在于提供了集中流动性功能, 集中流动性可以让用户自主选择做市价格区间, 这样就大大提高了资金利用的效率, 整个产品链上部分由若干个智能合约构成, 其大体分为核心合约(core contracts )与外围合约(periphery contracts)两部分组成, 外围合约主要包括 NonfungiblePositionManager, NonfungibleTokenPositionDescriptor, SwapRouter, TickLens, Quoter等合约, 核心合约主要包括 UniswapV3Pool, UniswapV3Factory等合约.

NonfungiblePositionManager: 直接与链下前端进行交互的外围合约, 主要对外提供比如创建流动池, 添加流动性, 删除流动性等API接口, 同时也会为每个流动性仓位生成一个唯一的NFT给到用户

SwapRouter: 直接与链下前端进行交互的外围合约, 主要对外提供token交易(兑换)API接口

NonfungibleTokenPositionDescriptor: NFT描述符, 记录一些NFT上有趣的信息, 这样可以在Opensea上展现出一些有意思的图形

TickLens: 外围合约, 链下前端可以通过它查询流动池中流动性仓位相关信息

Quoter: 外围合约, 链下前端可以通过它在真实交易前获取交易输出的数量

UniswapV3Factory: 核心合约, 流动池工厂, 所有的流动池合约都由这个工厂合约统一部署

UniswapV3Pool: 核心合约, 也是整个系统最核心的部分, 可以把它理解成一个流动性聚合器, 整合所有流动性, 统一处理token兑换功能

系统整体关系如下图所示:


如图所示:

1.链下前端用户调用NonfungiblePositionManager合约的流动池创建接口, 然后NonfungiblePositionManager合约内部会通过调用UniswapV3Factory工厂合约部署流动池合约UniswapV3Pool

2.链下前端用户调用NonfungiblePositionManager合约的添加流动性, 删除流动性等接口, 然后NonfungiblePositionManager合约内部会与UniswapV3Pool合约进行交互, 真正完成流动性添加删除等操作

3.链下前端用户调用SwapRouter合约的交易兑换接口, SwapRouter合约内部会再调用UniswapV3Pool合约的交易接口, 真正完成交易兑换过程

4.链下前端用户可以通过TickLens, Quoter两合约查询流动性信息和交易输出的数量, 而这两合约内部都会直接与对应的UniswapV3Pool合约进行交互, 完成真正的查询请求

在Uniswap V2版本中流动性的计算涉及K=xy, P=y/x等公式, 在V3中虽然是流动性聚合器, 更加复杂, 但是这些基本公式依然适用, 我们可以根据这些公式推导出:

这样我们在实际计算过程中不用关注x token和y token的数量, 而只需要关注K值和P值就可以完成交易输入输出数量的计算, 为了计算方便等原因, 实际代码中使用和保存的是根号P和L, L=根号K,
所以实际上从计算角度看是有两种视角:

视角一: x token和y token的数量, 如下图:

视角二:  根号P和L, 如下图:

以上两种视角是可以互相转换的

关于价格的表示问题:
因为价格是连续的, 存在精度的问题, 所以在实际计算中会非常麻烦, V3中通过一种巧妙的方式将连续的价格离散化, 然后每一个价格点位上都对应一个tick, V3中采用了等比数列的形式确定价格数列, 公比为 1.0001, 即下一个价格点为当前价格点的100.01%, 如下所示:

每个价格点位由一个tick表示, 将所有tick通过索引来表示, 定义整数i表示tick的索引:

每个流动性仓位只记录其 upper/lower tick 所包含的流动性元数据即可

所以价格有两种表示方式:
方式一: 直接用根号P来表示
方式二: 直接用tick索引i来表示价格点位

在contracts\libraries\TickMath.sol中包含两者转换的方法, 代码如下:

library TickMath {

    function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { ...
    }

    function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) { ...
    }

}

功夫编译系统心得总结

1.yarn工具可以理解为node下npm工具的升级版
2.cross-env,cross-var是node下的一个包(工具),用于解决跨平台环境变量设置问题
3.pipenv是一个python程序 用于建立python虚拟环境(带包管理器),Pipfile包含依赖,Pipfile.lock能有效解决依赖版本问题(注意和anconda中conda create创建的python环境在概念上进行区分和对比)
4.conan(柯南)也是一个python程序,用于C++包管理(配置执行文件:conanfile.py)
5.electron-builder electron程序编译工具,关于electron的简单理解就是把JavaScript,HTML 和 CSS和一个本地浏览器内核(包含v8-js解析引擎)打包到一起,成为一个可执行文件,而其中js代码是用ts代码开发,然后将ts代码通过typescript编译(转换)成js代码,关于typescript请自行百度
6.webpack js打包工具

core部分:
因为我装的anconda是3.8版本的,所以首先利用conda create -n创建了一个py37的环境,然后切换到这个py37的环境下,然后开始进行下面的工作
首先运行yarm 更新所有的node依赖包,然后生成yarn.lock文件,yarn.lock能有效解决依赖版本问题
然后利用pipenv安装python虚拟环境,并在这个环境下把所有需要的包装好
然后利用conan安装所有需要的C++包
然后调用python/build.py进行编译(内部通过cmake-js进行编译) 【2.2版本后改成利用conan通过conanfile.py文件操作
然后利用setup.py打包成wheel包【2.2版本后改成利用conan通过conanfile.py文件操作
然后通过python/build.py freeze打包成可执行文件(内部调用pyinstaller)【2.2版本后改成利用conan通过conanfile.py文件操作

app部分:
首先运行electron-builder install-app-deps安装相应依赖
然后调用.electron-kungfu/build.js和electron-builder进行编译和打包

功夫采用c/c++, python, js/ts/electron, js/node 混合开发,其中js/node和C++之间采用node的N-API机制交互,关于N-API请自行百度node-addon-api/node-gyp等。python与C++之间采用pybind11库进行混合编程, pybind11可实现C++11标准和Python之间的无缝操作,具体请自行百度

这里顺便说下cython机制, cython可以简单理解为开发了一种新的语言,这个语言语法类似python,然后编译的基本流程是首先通过Cython.Build.cythonize将cython代码编译(转换)为C++代码,然后通过setup函数调用本地安装的C++编译器将C++代码编译成二进制模块,然后就可以被python给调用了。开源项目AlgoPlus就采用了cython机制.

当我们用pip安装包(轮子文件)的时候,因为有些包内部采用了比如C++开发,并不一定是纯python,那么如果官方没有提供编译好的版本的话,那么就需要下载相应源码到本机进行实时编译,虽然这个过程是自动进行的,但是因为本地编译环境的原因,常常出错,所以我们一般 最好别用太新的版本的python,比如python3.7比较稳定发布时间也很长了,所以与之对应的包基本都有编译好的版本,出问题的概率相对较小,但如果用较新版本的python,比如3.8版本,甚至3.9版本,它们很多包仓库里面还没有编译好的版本,需要安装的时候本地编译,出错概率大大加大,所以开发建议用py37的版本。

多因子指数增强模型

项目概览:

本项目通过构建多因子模型实现了沪深300指数增强策略,流程包括原始数据获取、因子生成、因子预处理、单因子检验、收益预测模型及风险模型等,具体流程如图:

指数增强模型结构.png


第六弹(P2P分布式游戏下载软件)

技术虽然每天日新月异,但是对技术的热爱却从未改变,总结过去,展望未来。这次发的是当年开发的一套基于Boost C++和libtorrent的专门用于更新游戏的bt下载软件。软件界面如下:

«1234567»

Powered By Z-Blog 2.2 Prism Build 140101

Copyright phonegap.me Rights Reserved.