Uniswap v2-Liquidity

jerichou
2025-08-05 / 0 评论 / 1 阅读 / 正在检测是否收录...

一、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)。
  • 代币排序:按地址升序排列 token0token1,确保唯一性(避免重复创建 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)
  • 操作步骤

    1. 获取 UniswapV2Pair 合约的初始化字节码(creationCode)。
    2. token0token1 生成唯一的 salt
    3. 内联汇编调用 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 方法,设置代币地址 token0token1
  • 为什么单独初始化?

    • 分离部署和初始化,避免构造函数参数传递复杂性。
    • 确保代币顺序与 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

mdy2elj5.png

// 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 代币。


参数说明
参数类型作用
tokenAaddress代币 A 的地址
tokenBaddress代币 B 的地址
amountADesireduint用户希望添加的 A 代币数量
amountBDesireduint用户希望添加的 B 代币数量
amountAMinuint用户可接受的最少 A 代币实际添加量(防滑点)
amountBMinuint用户可接受的最少 B 代币实际添加量(防滑点)
toaddress接收 LP 代币的地址
deadlineuint交易过期时间(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 会:

    1. 检查交易对是否存在(若不存在则自动创建)。
    2. 根据当前池子比例计算最优的 amountAamountB(避免大幅改变价格)。
    3. 验证 amountA ≥ amountAMinamountB ≥ 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 合约的铸造函数:

    1. 根据添加的流动性比例,计算应铸造的 LP 代币数量。
    2. 将 LP 代币发送给 to 地址。
    3. 返回铸造的 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 添加流动性的核心内部逻辑,负责:

  1. 自动创建交易对(如果不存在)。
  2. 按最优比例计算实际添加量(防止改变市场价格)。
  3. 验证滑点限制amountAMinamountBMin)。

代码逻辑分步解析
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 合约中读取 reserve0reserve1(已按代币排序)。

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 足够匹配 amountADesiredamountBOptimal ≤ amountBDesired
      → 使用 amountADesiredamountBOptimal
    • Case 2:用户提供的 amountBDesired 不足 → 反向计算 amountAOptimal
(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 数量
  • 作用:计算用户实际转入的代币量(amount0amount1)。
  • 注意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); // 永久锁定初始流动性

mdy2fs2x.png

  • MINIMUM_LIQUIDITY(默认 1000 wei)被永久锁定,防止首次添加时 LP 代币被恶意操纵。
  • 初始流动性决定了 LP 代币的总供应基准。
情况2:非首次添加
liquidity = Math.min(
    amount0.mul(_totalSupply) / _reserve0, 
    amount1.mul(_totalSupply) / _reserve1
);

mdy2g4bz.png

按代币添加量的较小比例铸造 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

mdy2gpzd.png

// 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, tokenBaddress流动池中的两种代币地址
liquidityuint要销毁的 LP 代币数量
amountAMin, amountBMinuint用户能接受的最少提取量(滑点保护)
toaddress接收提取代币的地址
deadlineuint交易过期时间

执行流程分步解析
1. 权限与有效期检查
ensure(deadline) // 修饰器检查交易未过期
  • 防止用户签名交易被延迟执行后因价格波动造成损失。
2. 获取交易对地址
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
  • 通过工厂合约和代币地址计算 Pair 合约的确定性地址(CREATE2 生成)。
3. 转移 LP 代币到交易对
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity);
  • 前置条件:用户需提前 approve Router 合约操作其 LP 代币。
  • 将 LP 代币从用户转到 Pair 合约,准备销毁。
4. 销毁 LP 并计算赎回量
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
  • burn 函数内部逻辑

    1. 根据 LP 占总供应量的比例,计算应返还的代币量:

mdy2h9zg.pngamount0amount1 转给 to 地址。

  1. 销毁 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。
  • 注意balance0balance1 是当前合约的实际余额,可能包含未计入储备金的代币(如手续费)。
2. 手续费处理(_mintFee
bool feeOn = _mintFee(_reserve0, _reserve1);
  • 逻辑

    • 若协议手续费开启(0.05%),检查 kLast(上次手续费结算时的 reserve0*reserve1)与当前储备金的差值。
    • 若有手续费收益,铸造对应 LP 代币给工厂合约。
  • 影响:手续费会略微增加 totalSupply,从而影响后续 LP 销毁计算。
3. 计算应返还的代币量
amount0 = liquidity.mul(balance0) / _totalSupply;
amount1 = liquidity.mul(balance1) / _totalSupply;
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');
  • 数学公式

mdy2hu5x.png

  • 关键点

    • 使用 当前余额(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");
    }
}
1

评论 (0)

取消