合约部分目录:

/src包含了合约主要部分:接口/interfaces,依赖/lib,以及发挥功能的合约。/test下为测试合约
Uniswap V3 主要合约由两部分构成:
1.核心合约(core contracts)实现了最核心的功能,不提供用户友好的交互接口:UniswapV3Pool.sol
2.外围合约(periphery contracts)为核心合约实现了用户友好的接口:UniswapV3Manager.sol
token的流动:
Manager在Pool与用户之间,充当中介,通过接口继承以及函数重写,间接地在三方之间完成了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.sol和Position.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
测试mint和swap的函数都包含其中,两者逻辑相似,下面是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"
);
}
文章详细解析了UniswapV3合约的组成,包括核心合约UniswapV3Pool.sol实现基础功能,外围合约UniswapV3Manager.sol提供用户界面,以及合约之间的token流动机制。同时介绍了依赖的Ticker和Position库,并展示了如何通过测试合约进行功能验证。

5704





