Hello World

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');

使用 CREATE2 部署合约
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; // 反向映射
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
分叉主网测试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 代币。


参数说明
参数类型作用
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');
    _;
}

2. 计算实际添加量 _addLiquidity
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);

3. 获取交易对地址
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);

4. 代币转账到交易对
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);

5. 铸造 LP 代币
liquidity = IUniswapV2Pair(pair).mint(to);

// 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);
}

2. 获取当前储备金
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);

3. 处理空池情况
if (reserveA == 0 && reserveB == 0) {
    (amountA, amountB) = (amountADesired, amountBDesired);
}

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) 计算最优比例
(2) 滑点验证
// 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 数量

2. 手续费处理(_mintFee
bool feeOn = _mintFee(_reserve0, _reserve1);

3. 计算应铸造的 LP 数量

情况1:首次添加流动性(_totalSupply == 0

liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // 永久锁定初始流动性

情况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 代币

5. 更新储备金和触发事件
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1);
emit Mint(msg.sender, amount0, amount1);
分叉主网测试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, tokenBaddress流动池中的两种代币地址
liquidityuint要销毁的 LP 代币数量
amountAMin, amountBMinuint用户能接受的最少提取量(滑点保护)
toaddress接收提取代币的地址
deadlineuint交易过期时间

执行流程分步解析
1. 权限与有效期检查
ensure(deadline) // 修饰器检查交易未过期
2. 获取交易对地址
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
3. 转移 LP 代币到交易对
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity);
4. 销毁 LP 并计算赎回量
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);

amount0amount1 转给 to 地址。

  1. 销毁 LP 代币,更新储备金。
5. 代币排序与金额映射
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
6. 滑点验证
require(amountA >= amountAMin, 'INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'INSUFFICIENT_B_AMOUNT');
// 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 合约)
2. 手续费处理(_mintFee
bool feeOn = _mintFee(_reserve0, _reserve1);
3. 计算应返还的代币量
amount0 = liquidity.mul(balance0) / _totalSupply;
amount1 = liquidity.mul(balance1) / _totalSupply;
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');

4. 销毁 LP 并转账代币
_burn(address(this), liquidity); // 销毁LP
_safeTransfer(_token0, to, amount0); // 转代币0给用户
_safeTransfer(_token1, to, amount1); // 转代币1给用户
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); // 事件日志
分叉主网测试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");
    }
}

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »