加密日记【018】foundry-uniswap v3(2)
April 16th, 2023

工厂合约

用工厂合约来部署pool

  1. 它作为池子合约的中心化注册点。在工厂中,你可以找到所有已部署的池子,对应的 token 和地址。

  2. 它简化了池子合约的部署流程。EVM允许在智能合约中部署智能合约——工厂合约使用这个性质来让池子合约的部署变得十分简单。

  3. 它让池子合约的地址可预测,并且能够在注册池子之前就计算出这个地址。这让池子更容易被发现

智能合约部署有两个Opcode:CREATE和CREATE2

  1. CREATE 使用部署者账户的 nonce 来产生新的合约地址(伪代码如下):

    KECCAK256(deployer.address, deployer.nonce)
    

    nonce 是一个账户特定的交易计数器。在产生合约地址过程中使用 nonce 会使得在其他合约或者链下app中计算合约地址变得非常困难,主要是因为想要找到对应的 nonce,我们需要扫描账户的交易历史。

  2. CREATE2 使用一个特殊的*盐值(salt)*来产生合约地址。这是一个由开发者选择的任意序列,能够使得地址产生更加确定性(并降低碰撞概率):

    KECCAK256(deployer.address, salt, contractCodeHash)
    

工厂合约用的就是CREATE2,能够提前计算出地址

keccak256(abi.encodePacked(token0, token1, tickSpacing))

创建合约

pool = address(
    new UniswapV3Pool{
        salt: keccak256(abi.encodePacked(tokenX, tokenY, tickSpacing))
    }()
); //有salt参数的是CREATE2,没有参数的是CREATE

用控制反转存储和读取参数

// src/UniswapV3Factory.sol 存入参数
 parameters = PoolParameters({
            factory: address(this),
            token0: tokenX,
            token1: tokenY,
            tickSpacing: tickSpacing
        });

// src/IuniswapV3PoolDeployer.sol 读取参数
interface IUniswapV3PoolDeployer {
    struct PoolParameters {
        address factory;
        address token0;
        address token1;
        uint24 tickSpacing;
    }

    function parameters()
        external
        returns (
            address factory,
            address token0,
            address token1,
            uint24 tickSpacing
        );
}
// src/UniswapV3Pool.sol 通过接口读取参数
constructor() {
        (factory, token0, token1, tickSpacing) = IUniswapV3PoolDeployer(
            msg.sender
        ).parameters();
    }

CREATE2 创建pool函数:

pool = address(
    uint160(
        uint256(
            keccak256(
                abi.encodePacked(
                    hex"ff",
                    factory,
                    keccak256(
                        abi.encodePacked(token0, token1, tickSpacing)
                    ),
                    keccak256(type(UniswapV3Pool).creationCode)
                )
            )
        )
    )
);
  • 首先,我们计算盐值(abi.encodePacked(token0, token1, tickSpacing))并求哈希;

  • 接下来,我们获取池子合约的代码(type(UniswapV3Pool).creationCode)并求哈希;

  • 然后,我们构建这样一个字节序列:0xff,工厂合约地址,哈希后的盐值,哈希后的池子合约代码(EIP-1014用0xff来区分CREATECREATE2

  • 最后求这个序列的哈希并转换成地址。

将pool地址存入pools池中:

pools[tokenX][tokenY][tickSpacing] = pool;
pools[tokenY][tokenX][tickSpacing] = pool;

交易费用

只有流动性提供在价格区间才能收取手续费。需要判断两个点:

  1. 价格区间是否被激活,当价格上穿过tick上限和价格下穿tick下限。

  2. 价格区间的累积交易费用,比如区间内某个价格成交了会有一笔费用,如果去按个计算非常浪费gas,采用总费用减去价格区间之外的累积费用。

那么总费用是什么?价格区间之外的费用又如何计算?

这个费用是在用户swap的时候产生,是从用户提交token的金额里面减去一部分。这部分减去的金额会存储在池子里面,也就是说池子里面因为交易费用而增加一部分tokenx和tokeny,每单位的流动性累积的总费用由feeGrowthGlobal0X128feeGrowthGlobal1X128 两个状态变量表示,即总的费用/流动性。在计算真正的手续费时,需要使用 LP 数相乘来得出实际手续费数额,又因为 LP 数在不同价格可能时不同的(因为流动性深度不同),所以在计算手续费时只能针对 position 进行计算(同一个 position 内 LP 总量不变)。

这里有两个方面:swap时产生手续费,移除LP时获得手续费。所以用户swap时添加的手续费加入池子算出feeGrowthGlobal0X128feeGrowthGlobal1X128 ,在LP获取手续费时用单位费用*这个区间的LP。这个LP是

当价格穿过价格区间的tick时,记录区间外产生的费用。

例如这张图,当从下往上穿过lower tick 时记录当前的费用,当价格从上往下穿过时,记录当前的费用。

  • 提供流动性时,需要初始化 tick 对应的 fo 值

  • 发生交易时,需要更新 fg

  • 当交易过程中,当前价格穿过某一个 tick 时,需要更新此 tick 上的 fo 值

  • 当流动性发生变动时,更新此 position 中手续费的总和

Subscribe to 0x3c
Receive the latest updates directly to your inbox.
Nft graphic
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from 0x3c

Skeleton

Skeleton

Skeleton