Uniswap v2-Liquidity
一、Create pool
1.createPair
创建流动性时,需要确保交易对存在,如果不存在将会调用IUniswapV2Factory(factory).createPair创建交易对
// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
....
}这个函数是 UniswapV2Factory 的核心方法,用于动态创建新的代币交易对合约。
// Uniswap/v2-core/contracts/UniswapV2Factory.sol
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}参数校验
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');- 禁止相同代币:防止创建无意义的交易对(如 ETH/ETH)。
- 代币排序:按地址升序排列
token0和token1,确保唯一性(避免重复创建 ETH/USDT 和 USDT/ETH)。 - 零地址检查:防止无效代币。
- 重复创建检查:确保该交易对尚未存在。
使用 CREATE2 部署合约
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}CREATE2 特性:
- 通过
salt(代币对哈希)确定合约地址,地址可预测且与部署顺序无关。 - 公式:
address = hash(0xFF, factory, hash(token0, token1), init_code_hash)。
- 通过
操作步骤:
- 获取
UniswapV2Pair合约的初始化字节码(creationCode)。 - 用
token0和token1生成唯一的salt。 - 内联汇编调用
create2,返回新合约地址pair。
- 获取
当 Uniswap V2 开发时(2020 年),Solidity 尚未内置
new关键字支持CREATE2(直到 Solidity 0.8+ 才原生支持)。必须通过汇编直接调用 EVM 的CREATE2操作码。参数解释:
0:不发送 Ether(value)。add(bytecode, 32):跳过字节码的前 32 字节(长度字段),指向实际代码。mload(bytecode):读取字节码长度。salt:基于代币对的唯一标识(keccak256(token0, token1))。
初始化交易对
IUniswapV2Pair(pair).initialize(token0, token1);- 调用新
Pair合约的initialize方法,设置代币地址token0和token1。 为什么单独初始化?
- 分离部署和初始化,避免构造函数参数传递复杂性。
- 确保代币顺序与 Factory 一致。
更新工厂状态
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // 反向映射
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);记录交易对:
- 双向映射
getPair,支持通过任意代币顺序查询。 allPairs数组保存所有交易对地址,便于遍历。
- 双向映射
- 事件日志:通知监听者新交易对创建。
分叉主网测试createPair
function test_createPair() public {
ERC20 token = new ERC20("test", "TEST", 18);
address pair = factory.createPair(address(token), address(weth));
address token0 = IUniswapV2Pair(pair).token0();
address token1 = IUniswapV2Pair(pair).token1();
if (address(token) < WETH) {
assertEq(token0, address(token), "token 0");
assertEq(token1, WETH, "token 1");
} else {
assertEq(token0, WETH, "token 0");
assertEq(token1, address(token), "token 1");
}
}二、add liquidity
// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}该函数允许用户向 Uniswap V2 交易对(如 ETH/USDT)添加流动性,并获取对应的 LP 代币(流动性凭证)。主要步骤包括:计算最优添加量、代币转账、铸造 LP 代币。
参数说明
| 参数 | 类型 | 作用 |
|---|---|---|
tokenA | address | 代币 A 的地址 |
tokenB | address | 代币 B 的地址 |
amountADesired | uint | 用户希望添加的 A 代币数量 |
amountBDesired | uint | 用户希望添加的 B 代币数量 |
amountAMin | uint | 用户可接受的最少 A 代币实际添加量(防滑点) |
amountBMin | uint | 用户可接受的最少 B 代币实际添加量(防滑点) |
to | address | 接收 LP 代币的地址 |
deadline | uint | 交易过期时间(Unix 时间戳) |
代码逻辑分步解析
1. 修饰符 ensure(deadline)
modifier ensure(uint deadline) {
require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
_;
}- 作用:确保交易在
deadline之前被执行,否则回滚(防止过期交易被意外打包)。
2. 计算实际添加量 _addLiquidity
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);内部函数
_addLiquidity会:- 检查交易对是否存在(若不存在则自动创建)。
- 根据当前池子比例计算最优的
amountA和amountB(避免大幅改变价格)。 - 验证
amountA ≥ amountAMin和amountB ≥ amountBMin(防止高滑点损失)。
3. 获取交易对地址
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);pairFor:通过工厂合约和代币地址计算 Pair 合约地址(使用CREATE2确定性地址)。- 例如:
tokenA = WETH,tokenB = USDT→ 返回WETH/USDT交易对地址。
4. 代币转账到交易对
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);safeTransferFrom:安全地将代币从用户钱包转入交易对合约。- 需要用户提前授权(
approve)给 Router 合约。 - 若转账失败(如余额不足),会回滚交易。
- 需要用户提前授权(
5. 铸造 LP 代币
liquidity = IUniswapV2Pair(pair).mint(to);mint:调用 Pair 合约的铸造函数:- 根据添加的流动性比例,计算应铸造的 LP 代币数量。
- 将 LP 代币发送给
to地址。 - 返回铸造的 LP 数量(
liquidity)。
// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}该函数是 Uniswap V2 添加流动性的核心内部逻辑,负责:
- 自动创建交易对(如果不存在)。
- 按最优比例计算实际添加量(防止改变市场价格)。
- 验证滑点限制(
amountAMin和amountBMin)。
代码逻辑分步解析
1. 创建交易对(如果不存在)
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}作用:检查代币对是否存在,若不存在则通过工厂合约创建。
关键点:
- 使用
CREATE2确定性地址,确保同一代币对在不同网络的地址一致。 - 新创建的池子初始储备金为 0。
- 使用
2. 获取当前储备金
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);getReserves:从 Pair 合约中读取reserve0和reserve1(已按代币排序)。
3. 处理空池情况
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
}- 逻辑:若池子为空,直接接受用户提供的全部代币量(成为初始流动性提供者)。
示例:
- 用户首次添加
1 ETH + 2000 USDT→ 池子比例设为1:2000。
- 用户首次添加
4. 非空池的比例计算
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}(1) 计算最优比例
quote函数:根据当前池子比例计算匹配量。// 公式:amountBOptimal = (amountADesired * reserveB) / reserveA两种场景:
- Case 1:用户提供的
amountBDesired足够匹配amountADesired(amountBOptimal ≤ amountBDesired)
→ 使用amountADesired和amountBOptimal。 - Case 2:用户提供的
amountBDesired不足 → 反向计算amountAOptimal。
- Case 1:用户提供的
(2) 滑点验证
require(amountBOptimal >= amountBMin):
确保实际添加的amountB不低于用户设置的最小值(防止高滑点损失)。assert(amountAOptimal <= amountADesired):
内部安全检查(应恒成立,否则代码有误)。
// Uniswap/v2-core/contracts/UniswapV2Pair.sol
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}mint 是 UniswapV2 Pair 合约的核心函数,负责向流动性池添加流动性并铸造对应的 LP 代币。调用者为 Router 合约,用户通过 Router 间接调用。
关键步骤解析
1. 获取储备金和当前余额
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0); // 新增的 token0 数量
uint amount1 = balance1.sub(_reserve1); // 新增的 token1 数量- 作用:计算用户实际转入的代币量(
amount0和amount1)。 - 注意:
getReserves()返回的是上一次调用_update时的值,而balanceOf是实时余额。
2. 手续费处理(_mintFee)
bool feeOn = _mintFee(_reserve0, _reserve1);逻辑:
- 如果协议手续费开启(默认 0.05% 给工厂合约),检查是否需要铸造手续费对应的 LP 代币。
- 手续费计算基于
kLast(上次手续费结算时的reserve0 * reserve1)与当前储备金的差值。
- 目的:确保流动性提供者(LP)在提取流动性时支付应得的手续费。
3. 计算应铸造的 LP 数量
情况1:首次添加流动性(_totalSupply == 0)
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // 永久锁定初始流动性MINIMUM_LIQUIDITY(默认 1000 wei)被永久锁定,防止首次添加时 LP 代币被恶意操纵。- 初始流动性决定了 LP 代币的总供应基准。
情况2:非首次添加
liquidity = Math.min(
amount0.mul(_totalSupply) / _reserve0,
amount1.mul(_totalSupply) / _reserve1
);按代币添加量的较小比例铸造 LP,确保新增流动性不改变当前价格比例。
4. 校验与铸造 LP 代币
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity); // 给用户地址铸造 LP 代币- 防呆检查:防止零流动性铸造。
- ERC20 操作:通过
_mint将 LP 代币发送给用户。
5. 更新储备金和触发事件
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1);
emit Mint(msg.sender, amount0, amount1);_update:更新储备金为最新余额,并计算累计价格。kLast:记录当前储备金乘积,用于下次手续费计算。- 事件:通知外部流动性添加详情。
分叉主网测试addLiquidity
contract UniswapV2LiquidityTest is Test {
IWETH private constant weth = IWETH(WETH);
IERC20 private constant dai = IERC20(DAI);
IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02);
IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_WETH);
address private constant user = address(100);
function setUp() public {
// Fund WETH to user
deal(user, 100 * 1e18);
vm.startPrank(user);
weth.deposit{value: 100 * 1e18}();
weth.approve(address(router), type(uint256).max);
vm.stopPrank();
// Fund DAI to user
deal(DAI, user, 1000000 * 1e18);
vm.startPrank(user);
dai.approve(address(router), type(uint256).max);
vm.stopPrank();
}
function test_addLiquidity() public {
vm.prank(user);
(,, uint256 liquidity) = router.addLiquidity(WETH, DAI, 100 * 1e18, 1000000 * 1e18, 1, 1, user, block.timestamp);
console2.log("LP:", liquidity);
assertGt(pair.balanceOf(user), 0, "LP = 0");
assertEq(pair.balanceOf(user), liquidity, "user LP = liquidity");
}
}三、Remove liquidity
// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}该函数是 UniswapV2 Router 合约的核心方法,允许用户销毁 LP 代币并提取对应比例的两种底层代币。主要步骤包括:转移 LP 代币、销毁 LP 计算赎回量、滑点验证和代币转账。
参数说明
| 参数 | 类型 | 作用 |
|---|---|---|
tokenA, tokenB | address | 流动池中的两种代币地址 |
liquidity | uint | 要销毁的 LP 代币数量 |
amountAMin, amountBMin | uint | 用户能接受的最少提取量(滑点保护) |
to | address | 接收提取代币的地址 |
deadline | uint | 交易过期时间 |
执行流程分步解析
1. 权限与有效期检查
ensure(deadline) // 修饰器检查交易未过期- 防止用户签名交易被延迟执行后因价格波动造成损失。
2. 获取交易对地址
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);- 通过工厂合约和代币地址计算 Pair 合约的确定性地址(
CREATE2生成)。
3. 转移 LP 代币到交易对
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity);- 前置条件:用户需提前
approveRouter 合约操作其 LP 代币。 - 将 LP 代币从用户转到 Pair 合约,准备销毁。
4. 销毁 LP 并计算赎回量
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);burn函数内部逻辑:- 根据 LP 占总供应量的比例,计算应返还的代币量:
amount0 和 amount1 转给 to 地址。
- 销毁 LP 代币,更新储备金。
5. 代币排序与金额映射
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);- 将 Pair 合约返回的
amount0/amount1按用户输入的tokenA/tokenB顺序重新映射,确保接口友好性。
6. 滑点验证
require(amountA >= amountAMin, 'INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'INSUFFICIENT_B_AMOUNT');- 防止因价格波动或抢跑攻击导致用户提取量过少。
- 例如:用户设置
amountAMin = 0.9 ETH,若实际仅提取0.8 ETH,交易回滚。
// Uniswap/v2-core/contracts/UniswapV2Pair.sol
function burn(address to) external lock returns (uint amount0, uint amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}burn 是 UniswapV2 Pair 合约的关键函数,负责销毁 LP 代币并返还对应比例的底层代币。它是流动性移除的底层实现,由 Router 合约调用。
执行流程与关键机制
1. 状态读取与缓存(Gas 优化)
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
address _token0 = token0; // 代币0地址缓存
address _token1 = token1; // 代币1地址缓存
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)]; // 待销毁的LP数量(用户转到 pair 合约)- 目的:减少链上读取次数(
_reserve0、_token0等只读一次),节省 Gas。 - 注意:
balance0和balance1是当前合约的实际余额,可能包含未计入储备金的代币(如手续费)。
2. 手续费处理(_mintFee)
bool feeOn = _mintFee(_reserve0, _reserve1);逻辑:
- 若协议手续费开启(0.05%),检查
kLast(上次手续费结算时的reserve0*reserve1)与当前储备金的差值。 - 若有手续费收益,铸造对应 LP 代币给工厂合约。
- 若协议手续费开启(0.05%),检查
- 影响:手续费会略微增加
totalSupply,从而影响后续 LP 销毁计算。
3. 计算应返还的代币量
amount0 = liquidity.mul(balance0) / _totalSupply;
amount1 = liquidity.mul(balance1) / _totalSupply;
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');- 数学公式:
关键点:
- 使用 当前余额(
balance0) 而非 储备金(_reserve0),确保包含未结算的手续费,实现按比例公平分配。 - 必须返还两种代币(防止只提取单一代币的攻击)。
- 使用 当前余额(
4. 销毁 LP 并转账代币
_burn(address(this), liquidity); // 销毁LP
_safeTransfer(_token0, to, amount0); // 转代币0给用户
_safeTransfer(_token1, to, amount1); // 转代币1给用户- 原子性:先销毁 LP,再转账代币,防止重入攻击。
- 安全转账:
_safeTransfer会验证代币合约的返回值,防止恶意代币导致资金锁定。
5. 更新储备金与触发事件
balance0 = IERC20(_token0).balanceOf(address(this)); // 更新余额
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1); // 同步储备金
if (feeOn) kLast = uint(reserve0).mul(reserve1); // 更新kLast
emit Burn(msg.sender, amount0, amount1, to); // 事件日志_update:将最新余额写入储备金,并计算累计价格(用于 TWAP 预言机)。kLast:仅在手续费开启时更新,作为下次手续费计算的基准。
分叉主网测试removeLiquidity
contract UniswapV2LiquidityTest is Test {
IWETH private constant weth = IWETH(WETH);
IERC20 private constant dai = IERC20(DAI);
IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02);
IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_WETH);
address private constant user = address(100);
function setUp() public {
// Fund WETH to user
deal(user, 100 * 1e18);
vm.startPrank(user);
weth.deposit{value: 100 * 1e18}();
weth.approve(address(router), type(uint256).max);
vm.stopPrank();
// Fund DAI to user
deal(DAI, user, 1000000 * 1e18);
vm.startPrank(user);
dai.approve(address(router), type(uint256).max);
vm.stopPrank();
}
function test_removeLiquidity() public {
vm.startPrank(user);
(,, uint256 liquidity) = router.addLiquidity({
tokenA: DAI,
tokenB: WETH,
amountADesired: 1000000 * 1e18,
amountBDesired: 100 * 1e18,
amountAMin: 1,
amountBMin: 1,
to: user,
deadline: block.timestamp
});
pair.approve(address(router), liquidity);
(uint256 amountA, uint256 amountB) = router.removeLiquidity(WETH, DAI, liquidity, 1, 1, user, block.timestamp);
vm.stopPrank();
console2.log("DAI:", amountA);
console2.log("WETH:", amountB);
assertEq(pair.balanceOf(user), 0, "LP = 0");
}
}