UniswapV2相较于V1有了较大变动,也是目前最流行的dex版本,很多协议都是从UniswapV2 fork而来,在本系列的文章中,将使用Foundry作为合约的测试框架,使用solmate而非OpenZeppelin作为底层协议如ERC20的实现,将uniswap的核心框架代码从零到一进行复现。
UniswapV2的代码库分为core和periphery两部分,core包括:
periphery主要包括的合约是UniswapV2Router和UniswapV2Library,是重要的辅助交易合约
先从核心的添加流动性入手,uniswap在设计上与v1类似,交易者,lp,用户仍然是协议的核心参与者。与v1相比,v2的代码设计有所差异,lp注入流动性分为两部分,底层的实现是UniswapV2Pair合约,上层的入口在UniswapV2Router合约,在此我们先关注底层的实现。
添加流动性:即LP向交易对合约内按照一定比例转入两个底层资产,交易对为其按照投入的资产铸造出相应数量的流动性代币的过程。
作为UniswapV2Pair底层,这里需要实现的功能就是:计算用户投入多少底层资产,计算出相应额度的lp代币再mint给用户
function mint() public {
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
uint256 amount0 = balance0 - reserve0;
uint256 amount1 = balance1 - reserve1;
// 计算出本次用户投入的资产数量amount0和amount1
uint256 liquidity;
// 区分是否是首次mint,流动性代币的计算方式不同
if (totalSupply == 0) {
liquidity = ???
_mint(address(0), MINIMUM_LIQUIDITY);
} else {
liquidity = ???
}
if (liquidity <= 0) revert InsufficientLiquidityMinted();
// mint流动性代币
_mint(msg.sender, liquidity);
// 更新交易对内储备量缓存
_update(balance0, balance1);
emit Mint(msg.sender, amount0, amount1);
}
从代码中可以看出,这里的交易对会通过reserve0和reserve1两个变量来缓存当前交易对中两个token的数量,而不是直接使用balanceOf来计数(注:这里也是为了合约安全起见,避免被外部操控)
在调用mint 方法前,用户应该按照预期向当前合约转入token0和token1,这里再计算当前合约内的token余额balance0 和balance1 ,减去之前的缓存,得到的amount0和amount1 就是本次用户转入的token数量
在计算应该铸造出多少个lp代币时会区分totalSupply是否为0,即当前是否是初次提供流动性,假设当前交易对内的token情况如下所示:
token0 | token1 | liquidity |
---|---|---|
reserve0 | reserve1 | totalSupply |
amount0 | amount1 | totalSupply+lp |
按照固定的比例,本次待铸造的lp代币数目来源有两个,用户投入的两个token都可以作为基准来计算本次铸造的流动性代币
在实际开发中,UniswapV2的规则是选择两者中较小的那个,按照规定,用户提供的流动性是严格按照比例来的,两个值应该相等,但是若用户提供不平衡的流动性,这两个值就存在差异,如果协议按照大的来计算lp,那么相当于是对这种方式的鼓励,因此选择较小的流动性代币数目作为对用户的惩罚
回到totalSupply=0的条件分支,无法按照统一的比例计算lp代币数量,uniswapV2选择的是计算amount0*amount1的根号值,并且会统一减去MINIMUM_LIQUIDITY(1000)。
整理后的代码如下:
if (totalSupply == 0) {
liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
_mint(address(0), MINIMUM_LIQUIDITY);
} else {
liquidity = Math.min(
(amount0 * totalSupply) / _reserve0,
(amount1 * totalSupply) / _reserve1
);
}
配套的测试代码:
function testInitialMint() public {
vm.startPrank(lp);
token0.transfer(address(pair),1 ether);
token1.transfer(address(pair),1 ether);
pair.mint();
uint256 lpToken = pair.balanceOf(lp);
assertEq(lpToken, 1e18-1000);
}
function testExistLiquidity() public {
testInitialMint();
vm.startPrank(lp);
token0.transfer(address(pair),1 ether);
token1.transfer(address(pair),1 ether);
pair.mint();
uint256 lpToken = pair.balanceOf(lp);
assertEq(lpToken, 2e18-1000);
}
function testUnbalancedLiquidity() public {
testInitialMint();
vm.startPrank(lp);
token0.transfer(address(pair),2 ether);
token1.transfer(address(pair),1 ether);
pair.mint();
uint256 lpToken = pair.balanceOf(lp);
assertEq(lpToken, 2e18-1000);
}
从添加流动性的流程可以看出整体的流程是:用户转入底层资产token0和token1,mint出对应数目的lp代币
那么移除流动性就是逆向的过程,移除的前提是用户拥有lp代币,这里的lp代币就是用户提供流动性的凭证,具体的代码如下:
function burn() external{
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
uint256 liquidity = balanceOf[msg.sender];
// 计算用户的流动性占比的token数量
uint256 amount0 = liquidity * balance0 / totalSupply;
uint256 amount1 = liquidity * balance1 / totalSupply;
if (amount0 <=0 || amount1 <=0) revert InsufficientLiquidityBurned();
// 流动性代币burn
_burn(msg.sender, liquidity);
// 转移token回给用户
_safeTransfer(token0, msg.sender, amount0);
_safeTransfer(token1, msg.sender, amount1);
// 更新当前储备金
balance0 = IERC20(token0).balanceOf(address(this));
balance1 = IERC20(token1).balanceOf(address(this));
_update(balance0, balance1);
emit Burn(msg.sender, amount0, amount1);
}
测试代码如下:
function testBurn() public{
testInitialMint();
vm.startPrank(lp);
pair.burn();
assertEq(pair.balanceOf(lp), 0);
assertEq(token0.balanceOf(lp), 10 ether-1000);
assertEq(token1.balanceOf(lp), 10 ether-1000);
}
function testUnbalancedBurn() public {
testInitialMint();
vm.startPrank(lp);
token0.transfer(address(pair),2 ether);
token1.transfer(address(pair),1 ether);
pair.mint();
uint256 lpToken = pair.balanceOf(lp);
assertEq(lpToken, 2e18-1000);
pair.burn();
assertEq(pair.balanceOf(lp), 0);
assertEq(token0.balanceOf(lp), 10 ether-1500);
assertEq(token1.balanceOf(lp), 10 ether-1000);
}