milestone_1 第一笔交易

文章详细解析了UniswapV3合约的组成,包括核心合约UniswapV3Pool.sol实现基础功能,外围合约UniswapV3Manager.sol提供用户界面,以及合约之间的token流动机制。同时介绍了依赖的Ticker和Position库,并展示了如何通过测试合约进行功能验证。

合约部分目录:
在这里插入图片描述
/src包含了合约主要部分:接口/interfaces,依赖/lib,以及发挥功能的合约。/test下为测试合约

Uniswap V3 主要合约由两部分构成:
1.核心合约(core contracts)实现了最核心的功能,不提供用户友好的交互接口:UniswapV3Pool.sol
2.外围合约(periphery contracts)为核心合约实现了用户友好的接口:UniswapV3Manager.sol

token的流动:
ManagerPool与用户之间,充当中介,通过接口继承以及函数重写,间接地在三方之间完成了token的转移
在这里插入图片描述

1.UniswapV3Pool.sol

这是该项目的核心合约,下面来分析一下提供流动性部分的代码:

//依赖项
import "./interfaces/IERC20.sol";
import "./interfaces/IUniswapV3MintCallback.sol";
import "./interfaces/IUniswapV3SwapCallback.sol";

import "./lib/Position.sol";
import "./lib/Tick.sol";

在进入正题之前,我们先来看看该合约的依赖项:
其中Tick.solPosition.sol的逻辑类似,前者是用来储存区间信息并且内置一个更新信息的函数,后者是用来储存区间的实时流动性并且内置一个更新信息的函数。两者的函数都有一个有趣的用法,在接口里:mapping(int24 => Tick.Info) storage self都包含了一个引用自身的变量。有一点不同的是,后者的索引使用一个编码值:keccak256(abi.encodePacked(owner, lowerTick, upperTick)

Tick.sol

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.14;

library Tick {
    struct Info {
        bool initialized;//是否初始化标志
        uint128 liquidity;//区间流动性L
    }

    function update(//更新该区间的Info
        mapping(int24 => Tick.Info) storage self,
        int24 tick,
        uint128 liquidityDelta
    ) internal {
        Tick.Info storage tickInfo = self[tick];
        uint128 liquidityBefore = tickInfo.liquidity;
        uint128 liquidityAfter = liquidityBefore + liquidityDelta;

        if (liquidityBefore == 0) {
            tickInfo.initialized = true;
        }

        tickInfo.liquidity = liquidityAfter;
    }
}

Position.sol

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.14;

library Position {
    struct Info {
        uint128 liquidity;
    }

    function get(
        mapping(bytes32 => Info) storage self,
        address owner,
        int24 lowerTick,
        int24 upperTick
    ) internal view returns (Position.Info storage position) {
        position = self[
            keccak256(abi.encodePacked(owner, lowerTick, upperTick))
        ];
    }

    function update(Info storage self, uint128 liquidityDelta) internal {
        uint128 liquidityBefore = self.liquidity;
        uint128 liquidityAfter = liquidityBefore + liquidityDelta;

        self.liquidity = liquidityAfter;
    }
}

1.1 准备变量

首先,定义了几种映射类型来记录tick区间的位置,变化以及流动性

using Tick for mapping(int24 => Tick.Info);
using Position for mapping(bytes32 => Position.Info);
using Position for Position.Info;

区间的上下限,静态的token地址变量

int24 internal constant MIN_TICK = -887272;
int24 internal constant MAX_TICK = -MIN_TICK;

// Pool tokens, immutable
address public immutable token0;
address public immutable token1;

当前价格以及对应的tick

struct Slot0 {
    // Current sqrt(P)
    uint160 sqrtPriceX96;
    // Current tick
    int24 tick;
}

声明一些变量

Slot0 public slot0;

// Amount of liquidity, L.
uint128 public liquidity;

// Ticks info
mapping(int24 => Tick.Info) public ticks;//tick区间(编号=>[是否初始化,总流动性])
// Positions info
mapping(bytes32 => Position.Info) public positions;(编号=>[当前流动性])

构造器,初始化两种token的地址,当前价格,tick区间

constructor(
    address token0_,
    address token1_,
    uint160 sqrtPriceX96,
    int24 tick
) {
    token0 = token0_;
    token1 = token1_;

    slot0 = Slot0({sqrtPriceX96: sqrtPriceX96, tick: tick});
}

1.2 mint()

mint是铸币函数,接口提供区间相关信息,返回两种token的数量

function mint(
    address owner,//池子合约地址
    int24 lowerTick,//区间下界
    int24 upperTick,//区间下界
    uint128 amount,//区间流动性L
    bytes calldata data
) external returns (uint256 amount0, uint256 amount1) {

接下来是函数内容:

初始化ticks和positions变量

if (//检验上下界是否超出上下限
    lowerTick >= upperTick ||
    lowerTick < MIN_TICK ||
    upperTick > MAX_TICK
) revert InvalidTickRange();

if (amount == 0) revert ZeroLiquidity();

ticks.update(lowerTick, amount);//更新区间信息:上下界,流动性L
ticks.update(upperTick, amount);

Position.Info storage position = positions.get(//获取当前价格位置
    owner,
    lowerTick,
    upperTick
);
position.update(amount);//更新当前流动性
amount0 = 0.998976618347425280 ether; //根据上一章中算数计算来的
amount1 = 5000 ether; // TODO: replace with calculation

liquidity += uint128(amount);

然后是把铸币者的token转移到池子合约的地址上

uint256 balance0Before;
uint256 balance1Before;
if (amount0 > 0) balance0Before = balance0();
if (amount1 > 0) balance1Before = balance1();
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(//uniwswap实现的token转移接口
    amount0,
    amount1,
    data
);
if (amount0 > 0 && balance0Before + amount0 > balance0())
    revert InsufficientInputAmount();
if (amount1 > 0 && balance1Before + amount1 > balance1())
    revert InsufficientInputAmount();

emit Mint(
    msg.sender,
    owner,
    lowerTick,
    upperTick,
    amount,
    amount0,
    amount1
);

这里调用了IUniswapV3MintCallback接口:

https://github.com/Uniswap/v3-core/blob/main/contracts/interfaces/callback/IUniswapV3MintCallback.sol

普通用户地址难以实现 callback 函数,使用 callback 函数看起来很不用户友好,且合约无法信任第三方用户。所以调用者需要调用 uniswapV3MintCallback 来将 token 转给池子合约。调用 callback 函数后,我们会检查池子合约的对应余额是否发生变化,并且增量应该大于 amount0 和 amount1:这意味着调用者已经把钱转到了池子。

接下来,给出一个event:

emit Mint(
    msg.sender,
    owner,
    lowerTick,
    upperTick,
    amount,
    amount0,
    amount1
);

事件(Event)是合约数据在以太坊中标定的方式,后续可以据此进行搜索。通常来说,比较好的编程习惯是在合约的状态变量发生改变时发出一个事件,这能够让前端知道这件事情发生了。事件也包含了很多有用的信息,比如:调用者的地址,对应的流动性位置,上界和下界的 tick,新的流动性数量,两种 token 的数量。这些信息会作为日志(log)存储,任何人都可以通过收集这样的日志来重放合约中的状态变动,而不需要去遍历分析所有的区块和交易。

1.3 swap()

购币者与池子合约交易,逻辑与mint相似,本质都是与池子合约交换token,并且检验一下交易结果:

function swap(address recipient, bytes calldata data)
    public
    returns (int256 amount0, int256 amount1)
{
    //初始化交易量
    int24 nextTick = 85184;
    uint160 nextPrice = 5604469350942327889444743441197;

    amount0 = -0.008396714242162444 ether;
    amount1 = 42 ether;

    (slot0.tick, slot0.sqrtPriceX96) = (nextTick, nextPrice);

    IERC20(token0).transfer(recipient, uint256(-amount0));
    //开始交易
    uint256 balance1Before = balance1();
    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(//池子合约向购买者转出以及收取token
        amount0,
        amount1,
        data
    );
    if (balance1Before + uint256(amount1) > balance1())
        revert InsufficientInputAmount();
    //事务
    emit Swap(
        msg.sender,
        recipient,
        amount0,
        amount1,
        slot0.sqrtPriceX96,
        liquidity,
        slot0.tick
    );
}

2.UniswapV3Manager.sol

我们下一步的目标是将这个池子合约部署在一个本地的区块链上,并且使用一个前端应用与其交互。因此我们需要创建一个合约,能够让非合约的地址也与池子进行交互。
在这里插入图片描述

在这里插入图片描述

manager同样继承了Pool的接口,并且在内部完成了两种callback函数的重写,本质上就是一个中转站,使得token的传递多经过了一个点(实际上token只有在transfer中才会传递)。

3.测试

在部署我们的合约之前,我们需要写一系列的测试来保证合约功能正常。Forge 是一个绝妙的测试框架,能够让我们的测试十分简单。

一个示例的测试文件:

// test/UniswapV3Pool.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.14;

import "forge-std/Test.sol";

contract UniswapV3PoolTest is Test {
    function setUp() public {}

    function testExample() public {
        assertTrue(true);
    }
}

测试合约遵循以下规则:

1.setUp 函数用来准备测试样例。在每个测试样例中,我们都希望有一个配置好的环境,比如合约的部署、token的铸造、池子的初始化——这些都将在 setUp 中完成

2.每个测试样例以 test 开头,例如 testMint()。这能够让Forge区分出测试样例和其他的辅助函数(这里我们也可以写任何我们需要的辅助函数)

其中提供了

function assertEq(type a, type b, string memory err) internal;

判断a是等于b,否则提示错误err

文件目录:
在这里插入图片描述

3.1 ERC20Mintable.sol

Forge 能够用依赖的方式安装其他开源合约。在这里,我们需要包含铸造功能的 ERC20 合约。我们会使用 Solmate 的 ERC20 合约(Solmate 包含了一系列 gas 优化的合约),并且创建一个继承自 Solmate 合约的ERC20 合约,开放 mint 接口。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.14;

import "solmate/tokens/ERC20.sol";

contract ERC20Mintable is ERC20 {
    constructor(
        string memory _name,
        string memory _symbol,
        uint8 _decimals
    ) ERC20(_name, _symbol, _decimals) {}

    function mint(address to, uint256 amount) public {
        _mint(to, amount);
    }
}

3.2 UniswapV3Pool.t.sol

测试mintswap的函数都包含其中,两者逻辑相似,下面是testMintSuccess()
逻辑是先定义好各个参数,再通过断言判断结果

function testMintSuccess() public {
        //初始化测试参数
        TestCaseParams memory params = TestCaseParams({
            wethBalance: 1 ether,
            usdcBalance: 5000 ether,
            currentTick: 85176,
            lowerTick: 84222,
            upperTick: 86129,
            liquidity: 1517882343751509868544,
            currentSqrtP: 5602277097478614198912276234240,
            transferInMintCallback: true,
            transferInSwapCallback: true,
            mintLiqudity: true
        });
        (uint256 poolBalance0, uint256 poolBalance1) = setupTestCase(params);

        uint256 expectedAmount0 = 0.998976618347425280 ether;
        uint256 expectedAmount1 = 5000 ether;
        //以下的assertEq是测试一系列的参数在交易后是否正确
        assertEq(
            poolBalance0,
            expectedAmount0,
            "incorrect token0 deposited amount"
        );
        assertEq(
            poolBalance1,
            expectedAmount1,
            "incorrect token1 deposited amount"
        );
        assertEq(token0.balanceOf(address(pool)), expectedAmount0);
        assertEq(token1.balanceOf(address(pool)), expectedAmount1);

        bytes32 positionKey = keccak256(
            abi.encodePacked(address(this), params.lowerTick, params.upperTick)
        );
        uint128 posLiquidity = pool.positions(positionKey);
        assertEq(posLiquidity, params.liquidity);

        (bool tickInitialized, uint128 tickLiquidity) = pool.ticks(
            params.lowerTick
        );
        assertTrue(tickInitialized);
        assertEq(tickLiquidity, params.liquidity);

        (tickInitialized, tickLiquidity) = pool.ticks(params.upperTick);
        assertTrue(tickInitialized);
        assertEq(tickLiquidity, params.liquidity);

        (uint160 sqrtPriceX96, int24 tick) = pool.slot0();
        assertEq(
            sqrtPriceX96,
            5602277097478614198912276234240,
            "invalid current sqrtP"
        );
        assertEq(tick, 85176, "invalid current tick");
        assertEq(
            pool.liquidity(),
            1517882343751509868544,
            "invalid current liquidity"
        );
    }
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值