一、Flash Swap在 Uniswap 的 Flash Swap（闪电兑换） 机制中，要求交易后储备金满足 reserve_after >= reserve_before 的核心目的是为了确保 流动性提供者（LP）的本金安全 和 系统的偿付能力。以下是具体分析：Flash Swap 的基本流程Flash Swap 允许用户 无需预先支付代币 即可临时借出资金，但需在同一笔交易内完成还款。其关键步骤如下：借出代币：用户从交易对中借出代币（如 amount0Out > 0 或 amount1Out > 0）。执行回调：用户合约实现 uniswapV2Call，在回调中完成任意操作（如套利、还款）。还款验证：合约检查还款后的储备金是否满足 reserve_after >= reserve_before。// Uniswap/v2-core/contracts/UniswapV2Pair.sol function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); } _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); }当参数传递data时将开启 falsh swap分叉主网测试 flash swap// test/UniswapV2FlashSwap.sol // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; error InvalidToken(); contract UniswapV2FlashSwap { IUniswapV2Pair private immutable pair; address private immutable token0; address private immutable token1; constructor(address _pair) { pair = IUniswapV2Pair(_pair); token0 = pair.token0(); token1 = pair.token1(); } function flashSwap(address token, uint256 amount) external { if (token != token0 && token != token1) { revert InvalidToken(); } // 1. Determine amount0Out and amount1Out uint256 amount0Out = token == token0 ? amount : 0; uint256 amount1Out = token == token1 ? amount : 0; // 2. Encode token and msg.sender as bytes bytes memory data = abi.encode(token, msg.sender); // 3. Call pair.swap pair.swap(amount0Out, amount1Out, address(this), data); } // Uniswap V2 callback function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external { require(msg.sender == address(pair), "!pair"); require(sender == address(this), "!sender"); // 1. Require msg.sender is pair contract // 2. Require sender is this contract // Alice -> FlashSwap ---- to = FlashSwap ----> UniswapV2Pair // <-- sender = FlashSwap -- // Eve ------------ to = FlashSwap -----------> UniswapV2Pair // FlashSwap <-- sender = Eve -------- // 3. Decode token and caller from data (address token, address caller) = abi.decode(data, (address, address)); // 4. Determine amount borrowed (only one of them is > 0) uint256 amount = token == token0 ? amount0 : amount1; // 5. Calculate flash swap fee and amount to repay // fee = borrowed amount * 3 / 997 + 1 to round up uint256 fee = (amount * 3) / 997 + 1; uint256 amountToRepay = amount + fee; // 6. Get flash swap fee from caller IERC20(token).transferFrom(caller, address(this), fee); // 7. Repay Uniswap V2 pair IERC20(token).transfer(address(pair), amountToRepay); } } // test/UniswapV2FlashSwap.test.sol // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import {DAI, UNISWAP_V2_ROUTER_02, UNISWAP_V2_PAIR_DAI_WETH} from "../src/Constants.sol"; import {UniswapV2FlashSwap} from "./UniswapV2FlashSwap.sol"; contract UniswapV2FlashSwapTest is Test { IERC20 private constant dai = IERC20(DAI); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); UniswapV2FlashSwap private flashSwap; address private constant user = address(100); function setUp() public { flashSwap = new UniswapV2FlashSwap(UNISWAP_V2_PAIR_DAI_WETH); deal(DAI, user, 10000 * 1e18); vm.prank(user); dai.approve(address(flashSwap), type(uint256).max); // user -> flashSwap.flashSwap // -> pair.swap // -> flashSwap.uniswapV2Call // -> token.transferFrom(user, flashSwap, fee) } function test_flashSwap() public { uint256 dai0 = dai.balanceOf(UNISWAP_V2_PAIR_DAI_WETH); vm.prank(user); flashSwap.flashSwap(DAI, 1e6 * 1e18); uint256 dai1 = dai.balanceOf(UNISWAP_V2_PAIR_DAI_WETH); console2.log("DAI fee", dai1 - dai0); assertGe(dai1, dai0, "DAI balance of pair"); } }二、TwapUniswap V2 作为价格预言机存在被攻击的风险，主要源于其链上价格延迟性和流动性池的可操纵性。攻击者常通过 借贷协议（Lending Protocol） 或其他杠杆手段放大攻击效果。以下是具体分析：1. Uniswap V2 预言机的核心问题(1) 价格延迟机制Uniswap V2 的预言机基于 累计价格（Price Cumulative），通过记录每个区块第一笔交易的价格时间积分（TWAP）来平滑价格波动。但存在两个关键弱点：时间窗口依赖：短期价格波动仍可能被操纵（尤其在低流动性池中）。区块间隔影响：价格更新频率取决于区块生成速度（如以太坊约12秒/块）。(2) 流动性池的脆弱性低流动性池：小资金即可大幅改变价格。瞬时价格依赖：借贷协议可能直接读取瞬时价格（而非 TWAP），加剧风险。2. 典型攻击流程（结合借贷协议）以下是攻击者通过 操纵价格 → 欺骗借贷协议 → 套利获利 的常见步骤：步骤1：操纵 Uniswap 价格攻击手段：通过闪电贷借入大量代币（如 USDC）。在 Uniswap 低流动性池中大幅买入/卖出目标代币（如 ETH），扭曲瞬时价格。例如：用 100 万 USDC 买入 ETH，将池子价格从 1 ETH = 2000 USDC 推高到 1 ETH = 3000 USDC。借贷协议读取被操纵后的高价，认为抵押物价值虚高。步骤2：欺骗借贷协议攻击操作：以扭曲后的 ETH 价格作为抵押，借出超额其他资产（如 DAI）。例如：按 1 ETH = 3000 USDC 的虚假价格抵押 100 ETH，借出 200,000 DAI（实际 ETH 仅值 2000 USDC）。恢复 Uniswap 价格（通过反向交易或等待 TWAP 平滑）。步骤3：套利与获利最终获利：通过操纵价格虚增抵押物价值 → 借出超过抵押物实际价值的资金 → 放弃抵押物赚取差价。借贷协议出现坏账，损失由其他用户承担。3. 真实案例：Harvest Finance 攻击（2020）攻击细节：攻击者通过闪电贷借入大量 USDC。操纵 USDC/fUSDT 池的价格，导致 Harvest Finance 的质押合约高估 fUSDT 价值。存入少量 fUSDT 提取超额收益，获利约 2400 万美元。根本原因：协议直接依赖 Uniswap 的瞬时价格，未使用 TWAP 或交叉验证。4. 防御措施(1) 协议层面的改进使用 TWAP 而非瞬时价格：例如：Chainlink 或 Uniswap V3 的预言机，需至少 10 分钟的时间窗口平滑价格。多预言机交叉验证：结合 Chainlink、Uniswap TWAP 和中心化交易所价格，降低单点故障风险。流动性门槛：仅允许高流动性池（如 TVL > 100 万美元）作为价格源。(2) Uniswap V2 的局限性V2 的设计缺陷：累计价格仍可能被短时大额交易扭曲，尤其在低流动性池中。升级到 V3：Uniswap V3 提供更精细的预言机（如区间 TWAP），抗操纵性更强。// Uniswap/v2-core/contracts/UniswapV2Pair.sol function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); }这段代码负责 更新累计价格（price0CumulativeLast/price1CumulativeLast），用于实现 Uniswap V2 的 TWAP 预言机。其核心逻辑是：计算自上次更新后经过的时间（timeElapsed）。根据当前储备金比例（reserve1/reserve0 和 reserve0/reserve1）计算价格，并加权累加到历史数据中。price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;UQ112x112.encode(_reserve1).uqdiv(_reserve0)：将 _reserve1 编码为 UQ112x112) 定点数（112位整数 + 112位小数）。相当于左移 112 位执行定点数除法 reserve1reserve0reserve0reserve1，得到 token1 相对于 token0 的价格。\* timeElapsed：将价格按时间加权累加。例如：若 1 ETH = 2000 USDT（即 USDTETH=2000ETHUSDT=2000），且 timeElapsed = 30 秒，则累加 2000×30=60,0002000×30=60,000 到 price0CumulativeLast。价格部分占用 224 位，时间部分占用 32 位，刚好是 256 位所以乘法不会溢出加法溢出无害性TWAP 计算依赖差值：外部合约（如预言机）计算 TWAP 时，仅需两个时间点的累计价格差：TWAP=priceCumulative(t2)−priceCumulative(t1)t2−t1TWAP=t2−t1priceCumulative(t2)−priceCumulative(t1)即使 priceCumulative 溢出（回绕到 0），差值在模运算下仍正确。例如：priceCumulative(t1)=2256−1000priceCumulative(t1)=2256−1000priceCumulative(t2)=500priceCumulative(t2)=500（溢出后）实际差值：500−(2256−1000)≡1500mod 2256500−(2256−1000)≡1500mod2256（与未溢出时一致）。fork 主网测试// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol"; import {DAI, WETH, UNISWAP_V2_ROUTER_02, UNISWAP_V2_PAIR_DAI_WETH} from "../src/Constants.sol"; import {UniswapV2Twap} from "./UniswapV2Twap.sol"; contract UniswapV2TwapTest is Test { IERC20 private constant weth = IERC20(WETH); IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_WETH); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); uint256 private constant MIN_WAIT = 300; UniswapV2Twap private twap; function setUp() public { twap = new UniswapV2Twap(address(pair)); } function getSpot() internal view returns (uint256) { (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); // DAI / WETH return uint256(reserve0) * 1e18 / uint256(reserve1); } function swap() internal { deal(WETH, address(this), 100 * 1e18); weth.approve(address(router), type(uint256).max); address[] memory path = new address[](2); path[0] = WETH; path[1] = DAI; // Input token amount and all subsequent output token amounts router.swapExactTokensForTokens({ amountIn: 100 * 1e18, amountOutMin: 1, path: path, to: address(this), deadline: block.timestamp }); } function test_twap_same_price() public { skip(MIN_WAIT + 1); twap.update(); uint256 twap0 = twap.consult(WETH, 1e18); skip(MIN_WAIT + 1); twap.update(); uint256 twap1 = twap.consult(WETH, 1e18); assertApproxEqAbs(twap0, twap1, 1, "ETH TWAP"); } function test_twap_close_to_last_spot() public { // Update TWAP skip(MIN_WAIT + 1); twap.update(); // Get TWAP uint256 twap0 = twap.consult(WETH, 1e18); // Swap swap(); uint256 spot = getSpot(); console2.log("ETH spot price", spot); // Update TWAP skip(MIN_WAIT + 1); twap.update(); // Get TWAP uint256 twap1 = twap.consult(WETH, 1e18); console2.log("twap0", twap0); console2.log("twap1", twap1); // Check TWAP is close to last spot assertLt(twap1, twap0, "twap1 >= twap0"); assertGe(twap1, spot, "twap1 < spot"); } } // SPDX-License-Identifier: MIT pragma solidity >= 0.4 < 0.9; import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol"; import {FixedPoint} from "../src/uniswap-v2/FixedPoint.sol"; // Modified from https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol // Do not use this contract in production contract UniswapV2Twap { using FixedPoint for *; // Minimum wait time in seconds before the function update can be called again // TWAP of time > MIN_WAIT uint256 private constant MIN_WAIT = 300; IUniswapV2Pair public immutable pair; address public immutable token0; address public immutable token1; // Cumulative prices are uq112x112 price * seconds uint256 public price0CumulativeLast; uint256 public price1CumulativeLast; // Last timestamp the cumulative prices were updated uint32 public updatedAt; // TWAP of token0 and token1 // range: [0, 2**112 - 1] // resolution: 1 / 2**112 // TWAP of token0 in terms of token1 FixedPoint.uq112x112 public price0Avg; // TWAP of token1 in terms of token0 FixedPoint.uq112x112 public price1Avg; // Exercise 1 constructor(address _pair) { // 1. Set pair contract from constructor input pair = IUniswapV2Pair(_pair); // 2. Set token0 and token1 from pair contract token0 = pair.token0(); token1 = pair.token1(); // 3. Store price0CumulativeLast and price1CumulativeLast from pair contract (price0CumulativeLast, price1CumulativeLast) = (pair.price0CumulativeLast(), pair.price1CumulativeLast()); // 4. Call pair.getReserve to get last timestamp the reserves were updated // and store it into the state variable updatedAt (,, updatedAt) = pair.getReserves(); } // Exercise 2 // Calculates cumulative prices up to current timestamp function _getCurrentCumulativePrices() internal view returns (uint256 price0Cumulative, uint256 price1Cumulative) { // 1. Get latest cumulative prices from the pair contract (price0Cumulative, price1Cumulative) = (pair.price0CumulativeLast(), pair.price1CumulativeLast()); // If current block timestamp > last timestamp reserves were updated, // calculate cumulative prices until current time. // Otherwise return latest cumulative prices retrieved from the pair contract. // 2. Get reserves and last timestamp the reserves were updated from // the pair contract (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = pair.getReserves(); // 3. Cast block.timestamp to uint32 uint32 blockTimestamp = uint32(block.timestamp); if (blockTimestampLast != blockTimestamp) { // 4. Calculate elapsed time uint32 dt = blockTimestamp - blockTimestampLast; // Addition overflow is desired unchecked { // 5. Add spot price * elapsed time to cumulative prices. // - Use FixedPoint.fraction to calculate spot price. // - FixedPoint.fraction returns UQ112x112, so cast it into uint256. // - Multiply spot price by time elapsed price0Cumulative += uint256(FixedPoint.fraction(reserve1, reserve0)._x) * dt; price1Cumulative += uint256(FixedPoint.fraction(reserve0, reserve1)._x) * dt; } } } // Exercise 3 // Updates cumulative prices function update() external { // 1. Cast block.timestamp to uint32 uint32 blockTimestamp = uint32(block.timestamp); // 2. Calculate elapsed time since last time cumulative prices were // updated in this contract uint32 dt = blockTimestamp - updatedAt; // 3. Require time elapsed >= MIN_WAIT require(dt >= MIN_WAIT, "UniswapV2Twap: MIN_WAIT"); // 4. Call the internal function _getCurrentCumulativePrices to get // current cumulative prices (uint256 price0Cumulative, uint256 price1Cumulative) = _getCurrentCumulativePrices(); // Overflow is desired, casting never truncates // https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/building-an-oracle // Subtracting between two cumulative price values will result in // a number that fits within the range of uint256 as long as the // observations are made for periods of max 2^32 seconds, or ~136 years unchecked { // 5. Calculate TWAP price0Avg and price1Avg // - TWAP = (current cumulative price - last cumulative price) / dt // - Cast TWAP into uint224 and then into FixedPoint.uq112x112 price0Avg = FixedPoint.uq112x112(uint224((price0Cumulative - price0CumulativeLast) / dt)); price1Avg = FixedPoint.uq112x112(uint224((price1Cumulative - price1CumulativeLast) / dt)); } // 6. Update state variables price0CumulativeLast, price1CumulativeLast and updatedAt (price0CumulativeLast, price1CumulativeLast) = (price0Cumulative, price1Cumulative); updatedAt = blockTimestamp; } // Exercise 4 // Returns the amount out corresponding to the amount in for a given token function consult(address tokenIn, uint256 amountIn) external view returns (uint256 amountOut) { // 1. Require tokenIn is either token0 or token1 require(tokenIn == token0 || tokenIn == token1, "UniswapV2Twap: INVALID_TOKEN"); // 2. Calculate amountOut // - amountOut = TWAP of tokenIn * amountIn // - Use FixePoint.mul to multiply TWAP of tokenIn with amountIn // - FixedPoint.mul returns uq144x112, use FixedPoint.decode144 to return uint144 if (tokenIn == token0) { // Example // token0 = WETH // token1 = USDC // price0Avg = avg price of WETH in terms of USDC = 2000 USDC / 1 WETH // tokenIn = WETH // amountIn = 2 // amountOut = price0Avg * amountIn = 4000 USDC amountOut = FixedPoint.mul(price0Avg, amountIn).decode144(); } else { amountOut = FixedPoint.mul(price1Avg, amountIn).decode144(); } } } 三、Arb在 Uniswap V2 中，套利（Arbitrage） 是指利用不同交易所或同一交易所内的价格差异，通过低买高卖赚取无风险利润的行为。Uniswap V2 作为一个去中心化交易所（DEX），其价格完全由流动性池（Liquidity Pool）的算法决定，因此套利者在维持市场价格与全球市场价格一致方面扮演了关键角色。分叉主网测试套利策略1：使用本金直接套利。策略 2：借出 tokenIn → 兑换 → 反向兑换 → 归还 tokenIn。// UniswapV2Arb1.sol // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol"; import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; contract UniswapV2Arb1 { error UniswapV2Arb1__InsufficientProfit(); struct SwapParams { // Router to execute first swap - tokenIn for tokenOut address router0; // Router to execute second swap - tokenOut for tokenIn address router1; // Token in of first swap address tokenIn; // Token out of first swap address tokenOut; // Amount in for the first swap uint256 amountIn; // Revert the arbitrage if profit is less than this minimum uint256 minProfit; } function _swap(SwapParams memory params) internal returns (uint256 amountOut) { IERC20(params.tokenIn).approve(params.router0, params.amountIn); address[] memory path = new address[](2); path[0] = params.tokenIn; path[1] = params.tokenOut; uint256[] memory amounts = IUniswapV2Router02(params.router0).swapExactTokensForTokens({ amountIn: params.amountIn, amountOutMin: 0, path: path, to: address(this), deadline: block.timestamp }); IERC20(params.tokenOut).approve(params.router1, amounts[1]); path[0] = params.tokenOut; path[1] = params.tokenIn; amounts = IUniswapV2Router02(params.router1).swapExactTokensForTokens({ amountIn: amounts[1], amountOutMin: 1, path: path, to: address(this), deadline: block.timestamp }); amountOut = amounts[1]; } // - Execute an arbitrage between router0 and router1 // - Pull tokenIn from msg.sender // - Send amountIn + profit back to msg.sender function swap(SwapParams calldata params) external { IERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn); uint256 amountOut = _swap(params); if (amountOut - params.amountIn < params.minProfit) { revert UniswapV2Arb1__InsufficientProfit(); } IERC20(params.tokenIn).transfer(msg.sender, amountOut); } // - Execute an arbitrage between router0 and router1 using flash swap // - Borrow tokenIn with flash swap from pair // - Send profit back to msg.sender /** * @param pair Address of pair contract to flash swap and borrow tokenIn * @param isToken0 True if token to borrow is token0 of pair * @param params Swap parameters */ function flashSwap(address pair, bool isToken0, SwapParams calldata params) external { IUniswapV2Pair uniswap_pair = IUniswapV2Pair(pair); // 1. Determine amount0Out and amount1Out uint256 amount0Out = isToken0 ? params.amountIn : 0; uint256 amount1Out = isToken0 ? 0 : params.amountIn; // 2. Encode pair and msg.sender、params as bytes bytes memory data = abi.encode(msg.sender, pair, params); // 3. Call pair.swap uniswap_pair.swap(amount0Out, amount1Out, address(this), data); } function uniswapV2Call(address sender, uint256 amount0Out, uint256 amount1Out, bytes calldata data) external { require(sender == address(this), "!sender"); (address caller, address pair, SwapParams memory params) = abi.decode(data, (address, address, SwapParams)); require(msg.sender == pair, "!pair"); uint256 amountOut = _swap(params); // Calculate flash swap fee and amount to repay // fee = borrowed amount * 3 / 997 + 1 to round up uint256 fee = (params.amountIn * 3) / 997 + 1; uint256 amountToRepay = params.amountIn + fee; // uint256 profit = amountOut - amountToRepay; if (profit < params.minProfit) { revert UniswapV2Arb1__InsufficientProfit(); } IERC20(params.tokenIn).transfer(address(pair), amountToRepay); IERC20(params.tokenIn).transfer(caller, profit); } } //UniswapV2Arb1.test.sol // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; import {IWETH} from "../src/interfaces/IWETH.sol"; import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import { DAI, WETH, UNISWAP_V2_ROUTER_02, SUSHISWAP_V2_ROUTER_02, UNISWAP_V2_PAIR_DAI_WETH, UNISWAP_V2_PAIR_DAI_MKR } from "../src/Constants.sol"; import {UniswapV2Arb1} from "./UniswapV2Arb1.sol"; // Test arbitrage between Uniswap and Sushiswap // Buy WETH on Uniswap, sell on Sushiswap. // For flashSwap, borrow DAI from DAI/MKR pair contract UniswapV2Arb1Test is Test { IUniswapV2Router02 private constant uni_router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); IUniswapV2Router02 private constant sushi_router = IUniswapV2Router02(SUSHISWAP_V2_ROUTER_02); IERC20 private constant dai = IERC20(DAI); IWETH private constant weth = IWETH(WETH); address constant user = address(11); UniswapV2Arb1 private arb; function setUp() public { arb = new UniswapV2Arb1(); // Setup - WETH cheaper on Uniswap than Sushiswap deal(address(this), 100 * 1e18); weth.deposit{value: 100 * 1e18}(); weth.approve(address(uni_router), type(uint256).max); address[] memory path = new address[](2); path[0] = WETH; path[1] = DAI; uni_router.swapExactTokensForTokens({ amountIn: 100 * 1e18, amountOutMin: 1, path: path, to: user, deadline: block.timestamp }); // Setup - user has DAI, approves arb to spend DAI deal(DAI, user, 10000 * 1e18); vm.prank(user); dai.approve(address(arb), type(uint256).max); } function test_swap() public { uint256 bal0 = dai.balanceOf(user); vm.prank(user); arb.swap( UniswapV2Arb1.SwapParams({ router0: UNISWAP_V2_ROUTER_02, router1: SUSHISWAP_V2_ROUTER_02, tokenIn: DAI, tokenOut: WETH, amountIn: 10000 * 1e18, minProfit: 1 }) ); uint256 bal1 = dai.balanceOf(user); assertGe(bal1, bal0, "no profit"); assertEq(dai.balanceOf(address(arb)), 0, "DAI balance of arb != 0"); console2.log("profit", bal1 - bal0); } function test_flashSwap() public { uint256 bal0 = dai.balanceOf(user); vm.prank(user); arb.flashSwap( UNISWAP_V2_PAIR_DAI_MKR, true, UniswapV2Arb1.SwapParams({ router0: UNISWAP_V2_ROUTER_02, router1: SUSHISWAP_V2_ROUTER_02, tokenIn: DAI, tokenOut: WETH, amountIn: 60 * 1e18, minProfit: 1 }) ); uint256 bal1 = dai.balanceOf(user); assertGe(bal1, bal0, "no profit"); assertEq(dai.balanceOf(address(arb)), 0, "DAI balance of arb != 0"); console2.log("profit", bal1 - bal0); } } 策略3：借出 tokenOut → 卖出 → 归还 tokenIn。// UniswapV2Arb2.sol // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {IUniswapV2Pair} from "../../../src/interfaces/uniswap-v2/IUniswapV2Pair.sol"; import {IERC20} from "../../../src/interfaces/IERC20.sol"; error InsufficientProfit(); contract UniswapV2Arb2 { struct FlashSwapData { // Caller of flashSwap (msg.sender inside flashSwap) address caller; // Pair to flash swap from address pair0; // Pair to swap from address pair1; // True if flash swap is token0 in and token1 out bool isZeroForOne; // Amount in to repay flash swap uint256 amountIn; // Amount to borrow from flash swap uint256 amountOut; // Revert if profit is less than this minimum uint256 minProfit; } // Exercise 1 // - Flash swap to borrow tokenOut /** * @param pair0 Pair contract to flash swap * @param pair1 Pair contract to swap * @param isZeroForOne True if flash swap is token0 in and token1 out * @param amountIn Amount in to repay flash swap * @param minProfit Minimum profit that this arbitrage must make */ function flashSwap(address pair0, address pair1, bool isZeroForOne, uint256 amountIn, uint256 minProfit) external { // Write your code here // Don’t change any other code (uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(pair0).getReserves(); // Hint - use getAmountOut to calculate amountOut to borrow uint256 amountOut = isZeroForOne ? getAmountOut(amountIn, reserve0, reserve1) : getAmountOut(amountIn, reserve1, reserve0); bytes memory data = abi.encode( FlashSwapData({ caller: msg.sender, pair0: pair0, pair1: pair1, isZeroForOne: isZeroForOne, amountIn: amountIn, amountOut: amountOut, minProfit: minProfit }) ); IUniswapV2Pair(pair0).swap({ amount0Out: isZeroForOne ? 0 : amountOut, amount1Out: isZeroForOne ? amountOut : 0, to: address(this), data: data }); } function uniswapV2Call(address sender, uint256 amount0Out, uint256 amount1Out, bytes calldata data) external { // Write your code here // Don’t change any other code // NOTE - anyone can call FlashSwapData memory params = abi.decode(data, (FlashSwapData)); // Token in and token out from flash swap address token0 = IUniswapV2Pair(params.pair0).token0(); address token1 = IUniswapV2Pair(params.pair0).token1(); (address tokenIn, address tokenOut) = params.isZeroForOne ? (token0, token1) : (token1, token0); (uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(params.pair1).getReserves(); uint256 amountOut = params.isZeroForOne ? getAmountOut(params.amountOut, reserve1, reserve0) : getAmountOut(params.amountOut, reserve0, reserve1); IERC20(tokenOut).transfer(params.pair1, params.amountOut); IUniswapV2Pair(params.pair1).swap({ amount0Out: params.isZeroForOne ? amountOut : 0, amount1Out: params.isZeroForOne ? 0 : amountOut, to: address(this), data: "" }); // NOTE - no need to calculate flash swap fee IERC20(tokenIn).transfer(params.pair0, params.amountIn); uint256 profit = amountOut - params.amountIn; if (profit < params.minProfit) { revert InsufficientProfit(); } IERC20(tokenIn).transfer(params.caller, profit); } function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256 amountOut) { uint256 amountInWithFee = amountIn * 997; uint256 numerator = amountInWithFee * reserveOut; uint256 denominator = reserveIn * 1000 + amountInWithFee; amountOut = numerator / denominator; } } // UniswapV2Arb2.test.sol // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; import {IWETH} from "../src/interfaces/IWETH.sol"; import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import { DAI, WETH, UNISWAP_V2_ROUTER_02, UNISWAP_V2_PAIR_DAI_WETH, SUSHISWAP_V2_ROUTER_02, SUSHISWAP_V2_PAIR_DAI_WETH } from "../src/Constants.sol"; import {UniswapV2Arb2} from "./UniswapV2Arb2.sol"; // Test arbitrage between Uniswap and Sushiswap // Buy WETH on Uniswap, sell on Sushiswap. // For flashSwap, borrow DAI from DAI/MKR pair contract UniswapV2Arb2Test is Test { IUniswapV2Router02 private constant uni_router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); IUniswapV2Router02 private constant sushi_router = IUniswapV2Router02(SUSHISWAP_V2_ROUTER_02); IERC20 private constant dai = IERC20(DAI); IWETH private constant weth = IWETH(WETH); address constant user = address(11); UniswapV2Arb2 private arb; function setUp() public { arb = new UniswapV2Arb2(); // Setup - WETH cheaper on Uniswap than Sushiswap deal(address(this), 100 * 1e18); weth.deposit{value: 100 * 1e18}(); weth.approve(address(uni_router), type(uint256).max); address[] memory path = new address[](2); path[0] = WETH; path[1] = DAI; uni_router.swapExactTokensForTokens({ amountIn: 10 * 1e18, amountOutMin: 1, path: path, to: user, deadline: block.timestamp }); } function test_flashSwap() public { uint256 bal0 = dai.balanceOf(user); vm.prank(user); arb.flashSwap(UNISWAP_V2_PAIR_DAI_WETH, SUSHISWAP_V2_PAIR_DAI_WETH, true, 10000 * 1e18, 1); uint256 bal1 = dai.balanceOf(user); assertGe(bal1, bal0, "no profit"); assertEq(dai.balanceOf(address(arb)), 0, "DAI balance of arb != 0"); console2.log("profit", bal1 - bal0); } }
一、Create pool1.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 数组保存所有交易对地址，便于遍历。事件日志：通知监听者新交易对创建。分叉主网测试createPairfunction 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'); _; }作用：确保交易在 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。(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：记录当前储备金乘积，用于下次手续费计算。事件：通知外部流动性添加详情。分叉主网测试addLiquiditycontract 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);通过工厂合约和代币地址计算 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 函数内部逻辑：根据 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 代币给工厂合约。影响：手续费会略微增加 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：仅在手续费开启时更新，作为下次手续费计算的基准。分叉主网测试removeLiquiditycontract 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"); } }
Uniswap v2一、Swap单个币种之间swap的流程多个币种之间swap的流程1.swapExactTokensForTokens 和 swapTokensForExactTokens这个函数是 Uniswap V2 路由器合约中的一个重要功能，用于将确切数量的某种代币交换为另一种代币（可能经过多个交易对）。// v2-periphery/contracts/UniswapV2Router02.sol // swapExactTokensForTokens (精确输入，不确定输出) // 功能：用确切数量的输入代币换取尽可能多的输出代币 function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) { amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); } // swapTokensForExactTokens 精确输出，不确定输入 // 用尽可能少的输入代币换取确切数量的输出代币 function swapTokensForExactTokens( uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) { amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); }参数解释amountIn: 你想交换的输入代币数量amountOutMin: 你愿意接受的最小输出代币数量（防止高滑点）path: 交换路径（代币地址数组）to: 接收输出代币的地址deadline: 交易有效期限（Unix 时间戳）path 参数path 数组表示从代币 A 到代币 B 可能经过的所有中间代币。例如：直接交换：[代币A, 代币B]通过中间代币交换：[代币A, 代币WETH, 代币B]函数会按照路径顺序一一交换，直到最后一个目标币种。factory 参数factory 确实是指 UniswapV2Factory 合约（核心合约之一），它负责创建和管理交易对。amounts 数组amounts 是通过 UniswapV2Library.getAmountsOut() 计算得到的数组，它包含：沿交换路径每个阶段预期的代币数量数组的第一个元素是 amountIn（输入数量）最后一个元素是预期的输出代币数量中间元素是每个中间交换步骤的预期数量例如，对于路径 [A, B, C]，amounts 可能看起来像 [100 A, 50 B, 25 C]，表示：用 100 A 换 50 B再用 50 B 换 25 C函数执行流程计算沿路径的预期输出量 (getAmountsOut)检查最终输出是否满足用户的最小要求 (amountOutMin)将输入代币从用户转移到第一个交易对执行沿路径的交换 (_swap 内部函数)返回各阶段的实际交换数量这种设计允许复杂的多跳交换，同时确保用户获得至少他们指定的最小输出量。分叉主网测试getAmountsOut和getAmountsInforge test --fork-url $FORK_URL -vvv// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../../src/interfaces/IERC20.sol"; import {IWETH} from "../../src/interfaces/IWETH.sol"; import {IUniswapV2Router02} from "../../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import {DAI, WETH, MKR, UNISWAP_V2_ROUTER_02} from "../../src/Constants.sol"; contract UniswapV2SwapAmountsTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IERC20 private constant mkr = IERC20(MKR); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); function test_getAmountsOut() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountIn = 1e18; uint256[] memory amounts = router.getAmountsOut(amountIn, path); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); } function test_getAmountsIn() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountOut = 1e16; uint256[] memory amounts = router.getAmountsIn(amountOut, path); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); } } 2._swap最终执行两个token之间转换的核心函数 // v2-periphery/contracts/UniswapV2Router02.sol function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { for (uint i; i < path.length - 1; i++) { (address input, address output) = (path[i], path[i + 1]); (address token0,) = UniswapV2Library.sortTokens(input, output); uint amountOut = amounts[i + 1]; (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( amount0Out, amount1Out, to, new bytes(0) ); } }函数参数amounts：交易路径上每个代币的预期数量数组（由 getAmountsOut 计算得出）path：代币交换路径（如 [代币A, 代币B, 代币C]）_to：最终接收代币的地址核心逻辑分步解析步骤1：遍历交易路径for (uint i; i < path.length - 1; i++) { (address input, address output) = (path[i], path[i + 1]);循环处理路径中的每一跳（如 A→B→C 需要处理 A→B 和 B→C 两跳）input 和 output 分别表示当前交易的输入代币和输出代币步骤2：排序代币确定交易对(address token0,) = UniswapV2Library.sortTokens(input, output);Uniswap 交易对中的代币按地址排序存储（token0 < token1）通过排序确定代币在交易对中的顺序（影响 amount0Out 和 amount1Out 的赋值）步骤3：确定输出量uint amountOut = amounts[i + 1]; (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));amountOut 是当前交易对的预期输出量（来自 amounts 数组）关键规则：如果 input 是 token0，则 amount0Out = 0，amount1Out = amountOut（因为输入是 token0，输出是 token1）如果 input 是 token1，则 amount0Out = amountOut，amount1Out = 0（输入是 token1，输出是 token0）步骤4：确定接收地址address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;如果是中间交易（如 A→B→C 中的 A→B）：to 是下一个交易对的地址（B/C 的交易对）如果是最后一跳（如 B→C）：to 是用户指定的最终地址 _to步骤5：执行交易IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( amount0Out, amount1Out, to, new bytes(0) );通过 pairFor 计算当前交易对的地址调用交易对的 swap 方法，传递输出量和接收地址new bytes(0) 表示无回调数据（非闪电贷场景）_swap调用了IUniswapV2Pair的pairFor.swap函数// v2-core/contracts/UniswapV2Pair.sol // this low-level function should be called from a contract which performs important safety checks function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); } _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); }函数参数amount0Out：从储备中提取的 token0 数量amount1Out：从储备中提取的 token1 数量to：接收输出代币的地址data：回调数据（用于闪电贷等高级操作）主要逻辑步骤初步检查require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');确保至少有一个输出量大于0（不能两个都为0）使用getReserves来获取状态变量 在同一个快照时间获取数据保持数据一致性 同时这三个变量由于在一个存储槽内会被打包成一个sload操作节省gasuint112 private reserve0; // uses single storage slot, accessible via getReserves uint112 private reserve1; // uses single storage slot, accessible via getReserves uint32 private blockTimestampLast;为了安全会提前检查此时需要swap的两个token目前的余量require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');执行转账这块代码中使用了很多花括号 创建一个新的作用域（Scope），它可以用来 临时限制变量的生命周期，从而帮助解决 "Stack Too Deep"（堆栈过深） 的错误。Solidity 的 EVM 使用 栈（Stack） 来存储局部变量，但 栈的最大深度限制是 16 个槽位（即最多同时存储 16 个局部变量）。{ ... } 内部定义的变量只在块内有效，退出块后，这些变量 不再占用栈空间。这样，编译器可以 复用栈槽位，而不是持续占用新的位置。除了 { ... } 作用域隔离，还可以：使用 memory 或 calldata 存储临时数据（减少栈使用）。拆分成多个函数（减少单个函数的变量数量）。使用结构体（struct）或数组 打包变量（但可能增加 Gas 成本）。内联汇编（assembly） 手动管理栈（高级用法）。{ // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); }获取代币地址确保接收地址不是代币合约本身执行输出代币的转账（乐观转账）处理回调（闪电贷）if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);如果有回调数据，通知接收者（用于闪电贷等场景）计算实际输入量balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;获取新的余额计算实际输入量：如果新余额 > (旧储备 - 输出量)，差额就是输入量否则输入量为0先转账后检测，swap之前先讲要转换的token转账到币对合约兼容 ERC-20 标准：ERC-20 的 transferFrom 需要单独授权和转账操作，无法在单次调用中同时完成转入和交换。安全性：强制用户先转账可以防止重入攻击（转账和交换分离）。检查恒定乘积公式uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');计算调整后的余额（考虑0.3%手续费）确保 (adjustedBalance0 × adjustedBalance1) ≥ (旧储备0 × 旧储备1)这是Uniswap恒定乘积公式的核心检查更新状态_update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);更新储备量触发Swap事件function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); }(1) 溢出检查require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');作用：确保余额不超过 uint112 的最大值（$2^{112}-1$）。uint112(-1) 是 Solidity 中获取该类型最大值的方式（等价于 type(uint112).max）。(2) 时间处理uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired时间截断：block.timestamp % 2**32 确保时间戳在 uint32 范围内（约 136 年循环一次）。时间差计算：timeElapsed 允许溢出（如时间戳回绕时），因为 Uniswap 依赖时间差模运算的正确性。例如：若 blockTimestampLast = 2^32-1，blockTimestamp = 0，则 timeElapsed = 1（符合预期）。(3) 累计价格更新if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; }触发条件：时间已流逝（timeElapsed > 0）。储备金非零（防止除零错误）。价格计算：price0CumulativeLast：记录 token1/token0 的累计价格（单位为秒）。使用 UQ112x112 定点数库保持精度（112 位整数 + 112 位小数）。price1CumulativeLast：同理记录 token0/token1 的价格。为什么需要时间加权？抗操纵性市场价格可能被瞬时操纵（如闪电贷攻击）。TWAP 通过长时间累积平滑价格波动，外部合约可查询两个时间点的累计价差，再除以时间差得到平均价格：TWAP = priceCumulative(t2)−priceCumulative(t1) / (t2−t1)设计目的：支持链下价格预言机（如 TWAP 时间加权平均价格）。累计价格随时间线性增长，外部合约可通过差值计算时间段内的平均价格。(4) 储备金和时间戳更新reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1);更新状态：将实时余额写入储备金（reserve0/reserve1）。记录当前时间戳（blockTimestampLast）。事件：触发 Sync 事件，通知外部监听者储备金变化。分叉主网测试swapExactTokensForTokens和swapTokensForExactTokens// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; import {IWETH} from "../src/interfaces/IWETH.sol"; import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol"; import {DAI, WETH, MKR, UNISWAP_V2_PAIR_DAI_MKR, UNISWAP_V2_ROUTER_02} from "../../../src/Constants.sol"; contract UniswapV2SwapTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IERC20 private constant mkr = IERC20(MKR); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_MKR); address private constant user = address(100); function setUp() public { deal(user, 100 * 1e18); vm.startPrank(user); weth.deposit{value: 100 * 1e18}(); weth.approve(address(router), type(uint256).max); vm.stopPrank(); // Add MKR liquidity to DAI/MKR pool deal(DAI, address(pair), 1e6 * 1e18); deal(MKR, address(pair), 1e5 * 1e18); pair.sync(); } // Swap all input tokens for as many output tokens as possible function test_swapExactTokensForTokens() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountIn = 1e18; uint256 amountOutMin = 1; vm.prank(user); uint256[] memory amounts = router.swapExactTokensForTokens({ amountIn: amountIn, amountOutMin: amountOutMin, path: path, to: user, deadline: block.timestamp }); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); assertGe(mkr.balanceOf(user), amountOutMin, "MKR balance of user"); } // Receive an exact amount of output tokens for as few input tokens // as possible function test_swapTokensForExactTokens() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountOut = 0.1 * 1e18; uint256 amountInMax = 1e18; vm.prank(user); uint256[] memory amounts = router.swapTokensForExactTokens({ amountOut: amountOut, amountInMax: amountInMax, path: path, to: user, deadline: block.timestamp }); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); assertEq(mkr.balanceOf(user), amountOut, "MKR balance of user"); } }
{collapse}{collapse-item label="一、背景" open}传统的中心化交易所买家和卖家进行订单匹配，去中心的交易所引入开创了一种全新的交易方式，通过一种预设的数学公式（比如，常数乘积公式）创建一个流动性池，使得用户可以随时进行交易。自动做市商（Automated Market Maker，简称 AMM）是一种算法，或者说是一种在区块链上运行的智能合约，它允许数字资产之间的去中心化交易。{/collapse-item}{collapse-item label="二、常见的AMM算法" open} ## 1. 恒定乘积做市商（Constant Product Market Maker, CPMM）公式x * y = kx：代币A的数量y：代币B的数量k：恒定乘积（流动性池的总价值）特点代表协议：Uniswap V1/V2滑点机制：交易量越大，价格偏离越多（非线性）。流动性分布：全价格区间均匀分布，资本效率低。2. 恒定和做市商（Constant Sum Market Maker, CSMM）公式x + y = k特点零滑点：价格固定为 1:1（如稳定币交易）。缺点：流动性易耗尽（套利者会抽干某一代币）。代表协议：早期稳定币兑换池（已淘汰）。示例池中有 100 DAI + 100 USDC，用户用 10 DAI 换 10 USDC，池变为 110 DAI + 90 USDC。3. 恒定均值做市商（Constant Mean Market Maker, CMMM）公式x^a y^b z^c = ka,b,c：代币权重（可自定义）。特点支持多代币池（如Balancer的3种代币池）。灵活权重：例如 80% ETH + 20% BTC 的指数基金池。代表协议：Balancer。4. 集中流动性AMM（Concentrated Liquidity）代表协议：Uniswap V3原理LP可自定义价格区间（如 ETH/USDC 在 $1800-$2200 提供流动性）。公式分段处理：在区间内：使用 CPMM（x * y = k）。在区间外：流动性失效（变为单一资产）。优势资本效率提升：最高达4000倍（相比V2）。支持限价单：通过流动性区间模拟。5. 动态费用AMM（Dynamic Fees）代表协议：Trader Joe（Liquidity Book）特点根据市场波动动态调整手续费（如高波动时提高费率）。分档流动性：将价格划分为多个“档位”，每档独立计算流动性。AMM算法的核心问题与解决方案问题解决方案协议示例资本效率低集中流动性（Uniswap V3）Uniswap V3无常损失动态费率或对冲策略Bancor V3多代币池支持恒定均值算法（Balancer）Balancer总结CPMM（Uniswap）：简单通用，适合大部分代币对。CSMM：仅适合稳定币，已淘汰。CMMM（Balancer）：灵活支持多代币和自定义权重。集中流动性（Uniswap V3）：提升资本效率，适合专业做市。{/collapse-item}{collapse-item label="三、实现一个简单的swap" open} SimpleSwap 继承了 ERC20 代币标准，方便记录流动性提供者提供的流动性。在构造器中，我们指定一对代币地址 token0 和 token1，交易所仅支持这对代币。reserve0 和 reserve1 记录了合约中代币的储备量。contract SimpleSwap is ERC20 { // 代币合约 IERC20 public token0; IERC20 public token1; // 代币储备量 uint public reserve0; uint public reserve1; // 构造器，初始化代币地址 constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { token0 = _token0; token1 = _token1; } }交易所主要有两类参与者：流动性提供者（Liquidity Provider，LP）和交易者（Trader）。下面我们分别实现这两部分的功能。流动性提供流动性提供者给市场提供流动性，让交易者获得更好的报价和流动性，并收取一定费用。首先，我们需要实现添加流动性的功能。当用户向代币池添加流动性时，合约要记录添加的LP份额。这个LP份额主要是用户提供流动性的凭证，交易费的一定比例按 LP 份额分配给流动性提供者，同时也可以赎回代币，销毁 LP 代币，按LP比例取回 tokenA 和 tokenB,具体的tokenA和tokenB数量取决于他们现有数量然后乘LP比例。根据 Uniswap V2，LP份额如下计算：Δx 和 Δy 分别表示一笔交易中token和美元的变化量代币池被首次添加流动性时，LP份额 ΔL 由添加代币数量乘积的平方根决定:非首次添加流动性时，LP份额由添加代币数量占池子代币储备量的比例决定（两个代币的比例取更小的那个）:因为 SimpleSwap 合约继承了 ERC20 代币标准，在计算好LP份额后，可以将份额以代币形式铸造LP代币给用户。下面的 addLiquidity() 函数实现了添加流动性的功能，主要步骤如下：将用户添加的代币转入合约，需要用户事先给合约授权。根据公式计算添加的流动性份额，并检查铸造的LP数量。更新合约的代币储备量。给流动性提供者铸造LP代币。释放 Mint 事件。event Mint(address indexed sender, uint amount0, uint amount1); // 添加流动性，转进代币，铸造LP // @param amount0Desired 添加的token0数量 // @param amount1Desired 添加的token1数量 function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ // 将添加的流动性转入Swap合约，需事先给Swap合约授权 token0.transferFrom(msg.sender, address(this), amount0Desired); token1.transferFrom(msg.sender, address(this), amount1Desired); // 计算添加的流动性 uint _totalSupply = totalSupply(); if (_totalSupply == 0) { // 如果是第一次添加流动性，铸造 L = sqrt(x * y) 单位的LP（流动性提供者）代币 liquidity = sqrt(amount0Desired * amount1Desired); } else { // 如果不是第一次添加流动性，按添加代币的数量比例铸造LP，取两个代币更小的那个比例 liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); } // 检查铸造的LP数量 require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); // 给流动性提供者铸造liquidity个LP代币，代表他们提供的流动性 _mint(msg.sender, liquidity); emit Mint(msg.sender, amount0Desired, amount1Desired); }接下来，我们需要实现移除流动性的功能。当用户从池子中移除流动性 ΔL 时，合约要销毁LP份额代币，并按比例将代币返还给用户。返还代币的计算公式如下:下面的 removeLiquidity() 函数实现移除流动性的功能，主要步骤如下：获取合约中的代币余额。按LP的比例计算要转出的代币数量。检查代币数量。销毁LP份额。将相应的代币转账给用户。更新储备量。释放 Burn 事件。// 移除流动性，销毁LP，转出代币 // 转出数量 = (liquidity / totalSupply_LP) * reserve // @param liquidity 移除的流动性数量 function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { // 获取余额 uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); // 按LP的比例计算要转出的代币数量 uint _totalSupply = totalSupply(); amount0 = liquidity * balance0 / _totalSupply; amount1 = liquidity * balance1 / _totalSupply; // 检查代币数量 require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); // 销毁LP _burn(msg.sender, liquidity); // 转出代币 token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Burn(msg.sender, amount0, amount1); }至此，合约中与流动性提供者相关的功能完成了，接下来是交易的部分。交易在Swap合约中，用户可以使用一种代币交易另一种。那么我用 Δx单位的 token0，可以交换多少单位的 token1 呢？下面我们来简单推导一下。根据恒定乘积公式，交易前：交易后，有：交易前后 k 值不变，联立上面等式，可以得到：因此，可以交换到的代币数量 Δy 由 Δx，x，和 y 决定。注意 Δx 和 Δy 的符号相反，因为转入会增加代币储备量，而转出会减少。下面的 getAmountOut() 实现了给定一个资产的数量和代币对的储备，计算交换另一个代币的数量。// 给定一个资产的数量和代币对的储备，计算交换另一个代币的数量 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); amountOut = amountIn * reserveOut / (reserveIn + amountIn); }有了这一核心公式后，我们可以着手实现交易功能了。下面的 swap() 函数实现了交易代币的功能，主要步骤如下：用户在调用函数时指定用于交换的代币数量，交换的代币地址，以及换出另一种代币的最低数量。判断是 token0 交换 token1，还是 token1 交换 token0。利用上面的公式，计算交换出代币的数量。判断交换出的代币是否达到了用户指定的最低数量，这里类似于交易的滑点。将用户的代币转入合约。将交换的代币从合约转给用户。更新合约的代币储备量。释放 Swap 事件。// swap代币 // @param amountIn 用于交换的代币数量 // @param tokenIn 用于交换的代币合约地址 // @param amountOutMin 交换出另一种代币的最低数量 function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); if(tokenIn == token0){ // 如果是token0交换token1 tokenOut = token1; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance0, balance1); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); }else{ // 如果是token1交换token0 tokenOut = token0; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance1, balance0); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); } // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); }Swap 合约SimpleSwap 的完整代码如下：// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract SimpleSwap is ERC20 { // 代币合约 IERC20 public token0; IERC20 public token1; // 代币储备量 uint public reserve0; uint public reserve1; // 事件 event Mint(address indexed sender, uint amount0, uint amount1); event Burn(address indexed sender, uint amount0, uint amount1); event Swap( address indexed sender, uint amountIn, address tokenIn, uint amountOut, address tokenOut ); // 构造器，初始化代币地址 constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { token0 = _token0; token1 = _token1; } // 取两个数的最小值 function min(uint x, uint y) internal pure returns (uint z) { z = x < y ? x : y; } // 计算平方根 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) function sqrt(uint y) internal pure returns (uint z) { if (y > 3) { z = y; uint x = y / 2 + 1; while (x < z) { z = x; x = (y / x + x) / 2; } } else if (y != 0) { z = 1; } } // 添加流动性，转进代币，铸造LP // 如果首次添加，铸造的LP数量 = sqrt(amount0 * amount1) // 如果非首次，铸造的LP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP // @param amount0Desired 添加的token0数量 // @param amount1Desired 添加的token1数量 function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ // 将添加的流动性转入Swap合约，需事先给Swap合约授权 token0.transferFrom(msg.sender, address(this), amount0Desired); token1.transferFrom(msg.sender, address(this), amount1Desired); // 计算添加的流动性 uint _totalSupply = totalSupply(); if (_totalSupply == 0) { // 如果是第一次添加流动性，铸造 L = sqrt(x * y) 单位的LP（流动性提供者）代币 liquidity = sqrt(amount0Desired * amount1Desired); } else { // 如果不是第一次添加流动性，按添加代币的数量比例铸造LP，取两个代币更小的那个比例 liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); } // 检查铸造的LP数量 require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); // 给流动性提供者铸造LP代币，代表他们提供的流动性 _mint(msg.sender, liquidity); emit Mint(msg.sender, amount0Desired, amount1Desired); } // 移除流动性，销毁LP，转出代币 // 转出数量 = (liquidity / totalSupply_LP) * reserve // @param liquidity 移除的流动性数量 function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { // 获取余额 uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); // 按LP的比例计算要转出的代币数量 uint _totalSupply = totalSupply(); amount0 = liquidity * balance0 / _totalSupply; amount1 = liquidity * balance1 / _totalSupply; // 检查代币数量 require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); // 销毁LP _burn(msg.sender, liquidity); // 转出代币 token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Burn(msg.sender, amount0, amount1); } // 给定一个资产的数量和代币对的储备，计算交换另一个代币的数量 // 由于乘积恒定 // 交换前: k = x * y // 交换后: k = (x + delta_x) * (y + delta_y) // 可得 delta_y = - delta_x * y / (x + delta_x) // 正/负号代表转入/转出 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); amountOut = amountIn * reserveOut / (reserveIn + amountIn); } // swap代币 // @param amountIn 用于交换的代币数量 // @param tokenIn 用于交换的代币合约地址 // @param amountOutMin 交换出另一种代币的最低数量 function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); if(tokenIn == token0){ // 如果是token0交换token1 tokenOut = token1; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance0, balance1); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); }else{ // 如果是token1交换token0 tokenOut = token0; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance1, balance0); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); } // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); } }1.部署两个ERC20合约token0和token12.部署 SimpleSwap 合约，并将token0和token1的地址作为构造函数参数3.调用两个ERC20代币的approve()函数，分别给 SimpleSwap 合约授权 1000 单位代币。4.调用 SimpleSwap 合约的 addLiquidity() 函数给交易所添加流动性，token0 和 token1 分别添加 100 单位。5.调用 SimpleSwap 合约的 balanceOf() 函数查看用户的LP份额，这里应该为 100。6.调用 SimpleSwap 合约的 swap() 函数进行代币交易，用 100 单位的 token07.调用 SimpleSwap 合约的 reserve0 和 reserve1 函数查看合约中的代币储备粮，应为 200 和 50。上一步我们利用 100 单位的 token0 交换了 50 单位的 token 1{/collapse-item}{/collapse}