首页
Search
1
yamux: how to work?
79 阅读
2
The Art of Memory Allocation: Malloc, Slab, C++ STL, and GoLang Memory Allocation
70 阅读
3
How to receive a network packet in Linux
63 阅读
4
Maps and Memory Leaks in Go
54 阅读
5
C++ redis connection pool
52 阅读
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
登录
Search
标签搜索
web3
solidity
web3 security
c++
uniswapV3
redis
evm
uniswap
性能测试
k8s
wireguard
CNI
http
tailscale
nginx
linux
设计模式
Jericho
累计撰写
51
篇文章
累计收到
13
条评论
首页
栏目
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
页面
搜索到
29
篇与
的结果
2025-08-25
Uniswap v3-Part III
一、Tick BitmapTick1. tick 为什么不能直接存数组?tick 的范围是 int24,大约 -8,388,608, 8,388,607,总共 1677 万个 tick。如果直接用数组存一个 bool(1 byte),就要 16 MB;如果用 mapping(int24 => bool),存储上也非常浪费 gas。所以 V3 用了 bitmap 压缩存储。2. tick bitmap用一个 mapping(uint16 => uint256) 来存储。每个 uint256 有 256 位,每一位可以表示一个 tick 的占用状态。这样,一个 uint16 的 key,就能覆盖 256 个 tick。3. tick 是 int24,怎么分解?tick 是 int24(24 位),分成两部分来映射:前 16 位 → 作为 map 的 key(bucket id)。后 8 位 → 作为 uint256 里的 bit index。这样,tickBitmap[key] 是一个 256 位的整数,tick 的最后 8 位(0~255)表示它在这个 bucket 里的哪个位置。寻找 next tick1.背景Uniswap V3 里所有的 tick(价格刻度)不会全都存链上,只会存 有流动性提供者 LP 注入的 tick。这些 tick 被压缩存储在一个 bitmap 里:每个 word = 256 bits(uint256)每一位代表一个 tick 是否被初始化(有 LP)通过 tickSpacing 归约,避免 tick 太稠密所以问题是:给定当前 tick,怎么高效地找到 下一个被初始化的 tick?2. 核心优化思路传统方法:从当前位置逐个 tick 往前/往后扫描,直到找到一个被初始化的 tick。👉 时间复杂度 = O(n)(n = 相隔的 tick 数量)。Uniswap 方法:先用 position() 算出所在 word 和 bit 位置用 掩码(mask) 把 bitmap 剪裁到 需要的区间用 bitScan (O(1)) 算法定位到第一个 1👉 时间复杂度 = O(1) 常数级。3. 代码逐步解读(1) tick 压缩int24 compressed = tick / tickSpacing; if (tick < 0 && tick % tickSpacing != 0) compressed--; 将 tick 转换为压缩后的索引(对应 bitmap 的 bit)向负无穷取整,避免负数 tick 出错(2) 定位 word & bit(int16 wordPos, uint8 bitPos) = position(compressed);wordPos = compressed >> 8 (除以 256,定位到第几个 uint256)bitPos = compressed % 256 (在 word 内的第几位)(3) 当 (lte = true)当 Lte=true 时说明使用 token0=>token1 那么池子中的 token1 数量变少,token1/token0 的比值减少,意味着 tick 减小,要寻找的 next tick 小于等于当前的 tickuint256 mask = (1 << bitPos) - 1 + (1 << bitPos); uint256 masked = self[wordPos] & mask;mask 生成一个 从 bitPos 向右全是 1 的掩码masked 表示当前位置及其右边所有 tick 的状态initialized = masked != 0; next = initialized ? (compressed - int24(bitPos - BitMath.mostSignificantBit(masked))) * tickSpacing : (compressed - int24(bitPos)) * tickSpacing;如果找到 1:用 BitMath.mostSignificantBit(masked) 定位到最右边的 1如果没找到:直接返回 word 边界的 tick🔹 mostSignificantBit(uint256 x)核心逻辑就是 二分缩小范围:if (x >= 2^128) { x >>= 128; r += 128; } if (x >= 2^64) { x >>= 64; r += 64; } if (x >= 2^32) { x >>= 32; r += 32; } if (x >= 2^16) { x >>= 16; r += 16; } if (x >= 2^8) { x >>= 8; r += 8; } if (x >= 2^4) { x >>= 4; r += 4; } if (x >= 2^2) { x >>= 2; r += 2; } if (x >= 2) r += 1;每次比较,把候选范围折半。最多 8 次判断 + 位移 就能确定 MSB。时间复杂度:O(1)(常数时间,不依赖 256 位的大小)。(4) 当要寻找的 next tick 大于当前的 tick 时 (lte = false)(int16 wordPos, uint8 bitPos) = position(compressed + 1); uint256 mask = ~((1 << bitPos) - 1); uint256 masked = self[wordPos] & mask;mask 生成 bitPos 左边全是 1 的掩码masked 表示当前位置左边所有 tick 的状态initialized = masked != 0; next = initialized ? (compressed + 1 + int24(BitMath.leastSignificantBit(masked) - bitPos)) * tickSpacing : (compressed + 1 + int24(type(uint8).max - bitPos)) * tickSpacing;如果找到 1:用 BitMath.leastSignificantBit(masked) 定位到最近的 1如果没找到:返回 word 的边界 tick🔹 leastSignificantBit(uint256 x)逻辑稍微绕一点,它不是用 x & (-x)(也能做到),而是用 分组掩码 + 左右缩小范围:r = 255; if (x & type(uint128).max > 0) { r -= 128; } else { x >>= 128; } if (x & type(uint64).max > 0) { r -= 64; } else { x >>= 64; } if (x & type(uint32).max > 0) { r -= 32; } else { x >>= 32; } if (x & type(uint16).max > 0) { r -= 16; } else { x >>= 16; } if (x & type(uint8).max > 0) { r -= 8; } else { x >>= 8; } if (x & 0xf > 0) { r -= 4; } else { x >>= 4; } if (x & 0x3 > 0) { r -= 2; } else { x >>= 2; } if (x & 0x1 > 0) r -= 1;先看 低 128 位有没有非零,若有则最低位一定在 [0..127];否则一定在 [128..255]。再继续缩小到 64 位、32 位、16 位……一直到 1 位。r 从 255 开始往下减,最终就是 LSB 的位置。同样最多 8 步。时间复杂度:O(1)。二、Fee三、Twap1.Math Twap2.Math Swap token X Y3.codetickUniswap V3 不直接存价格,而是存储 tick(价格 log 的离散化形式)。价格 P = 1.0001^ticktick 累积(tickCumulative)用于计算时间加权价格。Observation每个 observation 存的是:blockTimestamp(记录的时间点)tickCumulative(从池子初始化以来,累加 tick * 时间)secondsPerLiquidityCumulativeX128(从池子初始化以来,累加 秒 / 流动性)initialized 标志位可以理解为:在某个时间点的累计账本TWAP 的本质TWAP = (tickCumulative_now - tickCumulative_past) / 时间间隔也就是:tick 在时间维度上的平均值。(然后再把 tick 转换成价格)📌 Uniswap V3 TWAP 更新流程用户发起 swap / mint / burn │ ▼ UniswapV3Pool.sol _update() │ ▼ Oracle.sol write(observation) └─ 记录当前 tick, 时间戳 └─ 维护 observation 环(环形数组)📌 Uniswap V3 TWAP 获取流程外部调用者 (合约/用户/Uniswap V3 Periphery) │ ▼ UniswapV3Pool.sol observe(uint32[] secondsAgos) │ ▼ Oracle.sol observe(...) ├─ 读取当前 observation 数据 ├─ 回溯 N 秒前的 observation └─ 计算 Δtick 累积值 / Δ时间 │ ▼ 返回平均价格 (TWAP) 注:Uniswap V3 Periphery调用 pool.observe([secondsAgo, 0])真正把 observation 数据转化为 TWAP 结果(即把 tickCumulative → 平均价格)🔄 逻辑流程初始化initialize() 在池子第一次创建时写入第一个 observation(时间戳=当前,累计量=0)。每次 swap 时写 observation在 pool swap 时,调用 Oracle.write(),往 observation 数组里写入一个新的 observation。写入内容:tickCumulative = last.tickCumulative + tick * ΔtsecondsPerLiquidityCumulativeX128 += Δt / liquidity时间戳更新到当前块这样,每个 observation 代表:从上一个 observation 到这次,tick 与流动性如何随时间演变。/// @notice Writes an oracle observation to the array /// @dev Writable at most once per block. Index represents the most recently written element. cardinality and index must be tracked externally. /// If the index is at the end of the allowable array length (according to cardinality), and the next cardinality /// is greater than the current one, cardinality may be increased. This restriction is created to preserve ordering. /// @param self The stored oracle array /// @param index The index of the observation that was most recently written to the observations array /// @param blockTimestamp The timestamp of the new observation /// @param tick The active tick at the time of the new observation /// @param liquidity The total in-range liquidity at the time of the new observation /// @param cardinality The number of populated elements in the oracle array /// @param cardinalityNext The new length of the oracle array, independent of population /// @return indexUpdated The new index of the most recently written element in the oracle array /// @return cardinalityUpdated The new cardinality of the oracle array function write( Observation[65535] storage self, uint16 index, uint32 blockTimestamp, int24 tick, uint128 liquidity, uint16 cardinality, uint16 cardinalityNext ) internal returns (uint16 indexUpdated, uint16 cardinalityUpdated) { Observation memory last = self[index]; // early return if we've already written an observation this block if (last.blockTimestamp == blockTimestamp) return (index, cardinality); // if the conditions are right, we can bump the cardinality if (cardinalityNext > cardinality && index == (cardinality - 1)) { cardinalityUpdated = cardinalityNext; } else { cardinalityUpdated = cardinality; } indexUpdated = (index + 1) % cardinalityUpdated; self[indexUpdated] = transform(last, blockTimestamp, tick, liquidity); }查询历史 observation调用 observe(secondsAgos[]) 时,会去算:当前时刻的累计值过去指定秒数的累计值再用差值 / 时间 = 平均值。function observe( Observation[65535] storage self, uint32 time, uint32[] memory secondsAgos, int24 tick, uint16 index, uint128 liquidity, uint16 cardinality ) internal view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) { require(cardinality > 0, 'I'); tickCumulatives = new int56[](secondsAgos.length); secondsPerLiquidityCumulativeX128s = new uint160[](secondsAgos.length); for (uint256 i = 0; i < secondsAgos.length; i++) { (tickCumulatives[i], secondsPerLiquidityCumulativeX128s[i]) = observeSingle( self, time, secondsAgos[i], tick, index, liquidity, cardinality ); } }4.fork 主网测试// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {TickMath} from "../src/uniswap-v3/TickMath.sol"; import {FullMath} from "../src/uniswap-v3/FullMath.sol"; import {IUniswapV3Pool} from "../src/interfaces/uniswap-v3/IUniswapV3Pool.sol"; contract UniswapV3Twap { IUniswapV3Pool public immutable pool; address public immutable token0; address public immutable token1; constructor(address _pool) { pool = IUniswapV3Pool(_pool); token0 = pool.token0(); token1 = pool.token1(); } // Copied from // https://github.com/Uniswap/v3-periphery/blob/0.8/contracts/libraries/OracleLibrary.sol /// @notice Given a tick and a token amount, calculates the amount of token received in exchange function getQuoteAtTick(int24 tick, uint128 baseAmount, address baseToken, address quoteToken) internal pure returns (uint256 quoteAmount) { uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself if (sqrtRatioX96 <= type(uint128).max) { uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; quoteAmount = baseToken < quoteToken ? FullMath.mulDiv(ratioX192, baseAmount, 1 << 192) : FullMath.mulDiv(1 << 192, baseAmount, ratioX192); } else { uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); quoteAmount = baseToken < quoteToken ? FullMath.mulDiv(ratioX128, baseAmount, 1 << 128) : FullMath.mulDiv(1 << 128, baseAmount, ratioX128); } } function getTwapAmountOut(address tokenIn, uint128 amountIn, uint32 dt) external view returns (uint256 amountOut) { // Task 1 - Require tokenIn is token0 or token1 // Task 2 - Assign tokenOut require(tokenIn == token0 || tokenIn == token1, "Invalid token"); address tokenOut = tokenIn == token0 ? token1 : token0; // Task 3 - Fill out timeDeltas with dt and 0 uint32[] memory timeDeltas = new uint32[](2); timeDeltas[0] = dt; timeDeltas[1] = 0; // Task 4 - Call pool.observe // Task 5 - Calculate tickCumulativeDelta (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = pool.observe(timeDeltas); int56 tickCumulativeDelta = tickCumulatives[1] - tickCumulatives[0]; // Task 6 - Calculate average tick int24 tick = int24(tickCumulativeDelta / int56(int32(dt))); // Always round to negative infinity if (tickCumulativeDelta < 0 && (tickCumulativeDelta % int56(uint56(dt)) != 0)) { tick--; } // Task 7 - Call getQuoteAtTick return getQuoteAtTick(tick, amountIn, tokenIn, tokenOut); } }// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {WETH, USDC, UNISWAP_V3_POOL_USDC_WETH_500} from "../src/Constants.sol"; import {UniswapV3Twap} from "./UniswapV3Twap.sol"; contract UniswapV3TwapTest is Test { UniswapV3Twap private twap; function setUp() public { twap = new UniswapV3Twap(UNISWAP_V3_POOL_USDC_WETH_500); } function test_twap() public { uint256 usdcOut = twap.getTwapAmountOut({tokenIn: WETH, amountIn: 1e18, dt: 3600}); console2.log("USDC out %e", usdcOut); } }九、Flashfork 主网测试// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {IERC20} from "../src/interfaces/IERC20.sol"; import {IUniswapV3Pool} from "../src/interfaces/uniswap-v3/IUniswapV3Pool.sol"; contract UniswapV3Flash { error UniswapV3Flash__NotAuthorized(); struct FlashCallbackData { uint256 amount0; uint256 amount1; address caller; } IUniswapV3Pool private immutable pool; IERC20 private immutable token0; IERC20 private immutable token1; constructor(address _pool) { pool = IUniswapV3Pool(_pool); token0 = IERC20(pool.token0()); token1 = IERC20(pool.token1()); } function flash(uint256 amount0, uint256 amount1) external { // Task 1 - ABI encode FlashCallbackData // Task 2 - Call IUniswapV3Pool.flash FlashCallbackData memory callbackData = FlashCallbackData({amount0: amount0, amount1: amount1, caller: msg.sender}); bytes memory data = abi.encode(callbackData); pool.flash(address(this), amount0, amount1, data); } function uniswapV3FlashCallback( // Pool fee x amount requested uint256 fee0, uint256 fee1, bytes calldata data ) external { // Task 3 - Check msg.sender is pool // Task 4 - Decode data into FlashCallbackData // Task 5 - Transfer fees from FlashCallbackData.caller // Task 6 - Repay pool, amount borrowed + fee if (msg.sender != address(pool)) revert UniswapV3Flash__NotAuthorized(); FlashCallbackData memory callbackData = abi.decode(data, (FlashCallbackData)); if (fee0 > 0) token0.transferFrom(callbackData.caller, address(this), fee0); if (fee1 > 0) token1.transferFrom(callbackData.caller, address(this), fee1); if (callbackData.amount0 > 0) token0.transfer(address(pool), callbackData.amount0 + fee0); if (callbackData.amount1 > 0) token1.transfer(address(pool), callbackData.amount1 + fee1); } } // 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 {UNISWAP_V3_POOL_DAI_WETH_3000, DAI, WETH} from "../src/Constants.sol"; import {UniswapV3Flash} from "./UniswapV3Flash.sol"; contract UniswapV3FlashTest is Test { IERC20 private constant weth = IERC20(WETH); IERC20 private constant dai = IERC20(DAI); UniswapV3Flash private uni; function setUp() public { uni = new UniswapV3Flash(UNISWAP_V3_POOL_DAI_WETH_3000); deal(DAI, address(this), 1e3 * 1e18); dai.approve(address(uni), type(uint256).max); } function test_flash() public { uint256 daiBefore = dai.balanceOf(address(this)); uni.flash(1e3 * 1e18, 0); uint256 daiAfter = dai.balanceOf(address(this)); uint256 fee = daiBefore - daiAfter; console2.log("DAI fee", fee); } }
2025年08月25日
16 阅读
0 评论
1 点赞
2025-08-16
Uniswap v3-Part I
Uniswap v3一、v3 与 v2 的差异1. 流动性Uniswap V2 和 V3 在流动性提供和管理方面有根本性的差异,主要体现在流动性的分布和效率上。Uniswap V2 的流动性:在 V2 中,流动性是均匀分布在整个价格曲线上(从 0 到无穷大)。流动性提供者(LP)将资金存入池子后,这些资金会根据恒定乘积公式(x * y = k)在所有可能的价格范围内可用。这意味着,无论当前价格如何,流动性都是“被动”分布的,导致资金利用率较低。因为在实际交易中,大部分交易发生在有限的价格范围内(如稳定币对的1%波动内),V2 的设计会造成大量资金闲置在极端价格区间,无法产生手续费收入。这使得 LP 的资本效率较低,容易遭受无常损失(Impermanent Loss),因为价格波动时,LP 的持仓会自动调整以维持 k 值不变。Uniswap V3 的流动性:V3 引入了“集中流动性”(Concentrated Liquidity)的概念,允许 LP 在特定价格范围内提供流动性,而不是整个曲线。这类似于限价订单,LP 可以选择价格区间(如在当前价格的 ±10% 内),从而将资金集中在高交易量的区域,提高资本效率。流动性被分割成“位置”(Positions),每个位置定义了一个价格范围 [a, b],在该范围内资金才被使用。超出范围的流动性会闲置,但 LP 可以主动调整位置以跟随市场价格。这种设计大大提高了资金利用率,据官方数据,V3 的资本效率可比 V2 高出 4000 倍以上(取决于范围选择)。然而,这也增加了复杂性,因为 LP 需要监控和调整位置以避免资金闲置或过度暴露于无常损失。总体差异:V2 的流动性更简单但效率低,适合被动投资者;V3 的流动性更灵活但需要主动管理,适合专业 LP。2. V3 主动管理流动性基于 ERC721,V2 被动管理流动性基于 ERC20这一方面直接延伸了流动性的差异,焦点在于流动性代币的标准和管理方式。Uniswap V2 的被动管理流动性基于 ERC20:在 V2 中,流动性由 ERC20 标准的 LP 代币表示。这些代币是同质化的(Fungible),每个池子的 LP 代币代表对池子总流动性的比例份额。LP 提供流动性后获得这些代币,可以自由转让、交易或用于其他 DeFi 协议(如抵押借贷)。管理是“被动”的:一旦提供流动性,资金就会自动根据 AMM 公式调整,无需 LP 干预。移除流动性时,按比例返还代币。这种设计简单,适合初学者,但缺乏灵活性,因为 LP 无法控制资金在价格曲线上的分布。Uniswap V3 的主动管理流动性基于 ERC721:V3 使用 ERC721 标准的 NFT(Non-Fungible Token)来表示流动性位置。每个位置都是独特的 NFT,包含特定价格范围、金额和所有者信息。这使得流动性是非同质化的,每个 NFT 代表一个自定义的价格区间。管理是“主动”的:LP 必须选择价格范围、监控市场,并定期调整位置(例如,通过重新定位 NFT 来跟随价格变动)。NFT 的设计允许更精细的控制,如合并多个位置或在二级市场交易 NFT 位置。这种基于 NFT 的方法提高了定制化,但也增加了 gas 费用和复杂性,因为每个位置都需要单独管理。差异总结:V2 的 ERC20 适合被动、标准化管理,易于集成;V3 的 ERC721 强调主动、个性化管理,更像投资策略,但门槛更高。3. 手续费的计算:V2 是固定 0.3%,而 V3 有四种计算方式手续费结构是 Uniswap 演进的核心变化,影响了 LP 收入和交易者成本。Uniswap V2 的手续费计算:V2 使用固定的 0.3% 手续费率,对每笔交易收取(例如,1000 USDC 交易收取 3 USDC)。这笔费用全部归 LP,所有池子统一费率,无需选择。简单明了,但不灵活,无法适应不同资产的波动性(如稳定币对需要低费率,波动币对需要高费率)。Uniswap V3 的手续费计算:V3 引入了多层级费率,允许池子创建者选择适合的费率。目前有四种费率选项:0.01%(超低费率,适合稳定币对,如 USDC/USDT)、0.05%(低费率,适合低波动对)、0.30%(标准费率,与 V2 类似,适合大多数对)、1.00%(高费率,适合高波动或 exotic 资产)。费率在池子创建时固定,不能更改。手续费计算基于交易金额乘以费率,但只在 LP 的价格范围内收取(超出范围不产生费用)。此外,V3 引入了“协议费”(Protocol Fee),允许 Uniswap 治理从 LP 费用中抽取一部分(目前为 0,但可通过治理激活)。这四种方式让池子更适应市场,但也导致流动性碎片化(同一对可能有多个费率池子)。差异:V2 的固定费率简单统一;V3 的多费率更市场化,但增加了选择复杂性。4. TWAP 的计算方式TWAP(Time-Weighted Average Price,时间加权平均价格)是 Uniswap 的 oracle 功能,用于提供可靠的价格数据,防止操纵。Uniswap V2 的 TWAP 计算:V2 使用累积价格(Cumulative Price)机制。每个池子维护两个累积变量:price0CumulativeLast 和 price1CumulativeLast,分别记录 token0 和 token1 的时间加权价格(价格 * 时间增量)。TWAP 通过 (当前累积价格 - 上次累积价格) / 时间间隔 计算得出,支持查询任意时间段的平均价格。但 V2 的 oracle 较基础,容易受短期操纵影响(因为依赖最后区块的价格),且计算 gas 消耗较高。外部合约需手动计算 TWAP,通常需要至少 1 小时间隔以防操纵。Uniswap V3 的 TWAP 计算:V3 显著改进了 oracle,引入了“观察数组”(Observations Array),一个固定大小(默认 500)的环形缓冲区,存储历史累积价格和时间戳。TWAP 使用二分查找从数组中插值计算,支持更精确的短期 TWAP(低至 9 秒)。计算公式类似 V2,但更高效:TWAP = (累积价格差 / 时间差),并使用几何平均来处理价格。V3 的 pair 合约内置 observe 函数,直接返回指定时间点的 TWAP 数组,减少了 gas 消耗并提高了抗操纵性(因为历史数据不可篡改)。此外,V3 支持“虚拟观察”(Virtual Observations),允许在不更新数组时估算当前 TWAP。差异:V2 的 TWAP 简单但易操纵、gas 高;V3 的 TWAP 更精确、抗操纵强、效率高,适合 DeFi 集成如借贷协议。5. Uniswap V3 的优点和缺点Uniswap V3 相对于 V2 的改进带来了显著优势,但也引入了新挑战。优点更高的资本效率:集中流动性允许 LP 用更少的资金提供等效深度,潜在收益更高(手续费收入可增加数倍)。灵活性增强:多费率和自定义价格范围让 LP 像做市商一样操作,适应不同市场条件。更好的 oracle:改进的 TWAP 提供更可靠的价格数据,支持更多 DeFi 用例如衍生品。gas 优化:尽管复杂,V3 的核心交易 gas 费用与 V2 相当,甚至在某些场景更低。创新潜力:NFT 位置启用新功能,如流动性挖矿激励针对特定范围,或位置的金融化(借贷 NFT)。缺点复杂性增加:主动管理要求 LP 具备市场知识,否则可能遭受更高无常损失或资金闲置。初学者容易出错。流动性碎片化:多费率池子导致同一交易对的流动性分散,可能增加滑点。更高风险:价格范围选择不当可能导致位置“出界”(Out-of-Range),资金不产生收入;此外,MEV(矿工可提取价值)攻击更常见。gas 费用波动:调整位置或创建新池子 gas 更高,适合大额 LP。采用门槛:V3 的 NFT 和范围管理让集成更难,部分用户仍偏好 V2 的简单性。二、uniswap v3 架构数学公式fork 主网测试计算价格pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IUniswapV3Pool} from "../src/interfaces/uniswap-v3/IUniswapV3Pool.sol"; import {UNISWAP_V3_POOL_USDC_WETH_500} from "../src/Constants.sol"; import {FullMath} from "../src/uniswap-v3/FullMath.sol"; contract UniswapV3SwapTest is Test { // token0 (X) uint256 private constant USDC_DECIMALS = 1e6; // token1 (Y) uint256 private constant WETH_DECIMALS = 1e18; // 1 << 96 = 2 ** 96 uint256 private constant Q96 = 1 << 96; IUniswapV3Pool private immutable pool = IUniswapV3Pool(UNISWAP_V3_POOL_USDC_WETH_500); // - Get price of WETH in terms of USDC and return price with 18 decimals function test_spot_price_from_sqrtPriceX96() public { uint256 price = 0; IUniswapV3Pool.Slot0 memory slot0 = pool.slot0(); // sqrtPriceX96 * sqrtPriceX96 might overflow // So use FullMath.mulDiv to do uint256 * uint256 / uint256 without overflow // price = FullMath.mulDiv(slot0.sqrtPriceX96, slot0.sqrtPriceX96, Q96); price = FullMath.mulDiv(slot0.sqrtPriceX96, slot0.sqrtPriceX96, Q96); // 1 / price = 1 / (P * Q96) price = 1e12 * 1e18 * Q96 / price; assertGt(price, 0, "price = 0"); console2.log("price %e", price); } } 三、SwapSwap 算法Ticks 是算法自动划分的数学网格,覆盖从负无穷到正无穷的价格范围(实际存储有边界)在 tickLower 和 tickUpper 区间内,流动性为 L,当价格 p 朝着 ticklower 移动时,移动出 ticklower 后,最新的流动性等于当前流动性减去▲L,如果朝着 tickupper 移动时,移动出 tickupper 后,最新的流动性等于当前流动性加上▲L。当价格从 ticklower 左侧移动到右侧时最新的流动性等于当前流动性加上▲L,如果价格从 tickupper 右侧移动到左侧时最新的流动性等于当前流动性减去▲L。同理可以推导出增减 y 时的价格Swap 实现swap 函数是 Uniswap V3 核心流动性池的核心交易实现,它就是在链上把 “用户输入一定 token,沿着价格曲线撮合流动性,跨 tick 调整价格,收取手续费,更新全局状态1️⃣ 前置检查 & 交易方向require(amountSpecified != 0, 'AS'); ... require( zeroForOne ? sqrtPriceLimitX96 < slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 > TickMath.MIN_SQRT_RATIO : sqrtPriceLimitX96 > slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 < TickMath.MAX_SQRT_RATIO, 'SPL' ); slot0.unlocked = false;amountSpecified > 0 → Exact In 模式amountSpecified < 0 → Exact Out 模式zeroForOne 表示交易方向(token0 → token1)检查价格限制 sqrtPriceLimitX96 是否合理,防止价格滑到极端值锁池(防重入)2️⃣ 初始化交易缓存SwapCache memory cache = {...}; SwapState memory state = {...};SwapCache 保存本次交易不变的信息(初始流动性、协议费参数、当前时间等)SwapState 是动态状态(剩余的 amount、当前价格、tick、全局手续费累积、流动性等)3️⃣ 核心循环(逐 Tick 模拟)while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) { StepComputations memory step; ... (state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(...);这个循环就是 沿着价格曲线一步步执行 swap,直到:用完用户的输入(Exact In)或达到目标输出(Exact Out)或触及价格限制每一步(step):找到下一个已初始化的 tick (tickNext)计算从当前价格走到下一个 tick 或价格限制需要的量调用 SwapMath.computeSwapStep:根据恒定乘积公式 + 手续费算出:amountIn(净输入)amountOut(输出)feeAmount(手续费)sqrtPriceX96(新的价格)更新剩余需求(amountSpecifiedRemaining)更新 feeGrowthGlobalX128(全局手续费累积)如果跨过一个已初始化的 tick:触发 ticks.cross,调整流动性(增加或减少)更新当前 tick4️⃣ 更新价格 & 流动性状态循环结束后:如果 tick 变了,更新 slot0(价格、tick、oracle 观测点)更新全局流动性 liquidity(如果跨 tick 流动性发生变化)更新全局手续费计数器 & 协议费记录5️⃣ 计算最终的交易结果(amount0, amount1) = zeroForOne == exactInput ? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated) : (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);根据方向和模式算出:用户实际输入了多少 token0/token1用户实际收到了多少 token0/token16️⃣ 资金结算if (zeroForOne) { if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1)); ... IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data); } else { ... }如果用户要收的币(amountOut)是正数,就直接 safeTransfer 给用户调用 uniswapV3SwapCallback 让调用方支付输入币检查支付是否足额(余额变化)7️⃣ 事件 & 解锁emit Swap(...); slot0.unlocked = true;发出 Swap 事件(链上日志)解锁池子Router 提供的 swap接口函数调用链fork 主网测试 swap 的接口// 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 {ISwapRouter} from "../src/interfaces/uniswap-v3/ISwapRouter.sol"; import {DAI, WETH, WBTC, UNISWAP_V3_SWAP_ROUTER_02} from "../src/Constants.sol"; contract UniswapV3SwapTest is Test { IWETH private weth = IWETH(WETH); IERC20 private dai = IERC20(DAI); IERC20 private wbtc = IERC20(WBTC); ISwapRouter private router = ISwapRouter(UNISWAP_V3_SWAP_ROUTER_02); uint24 private constant POOL_FEE = 3000; function setUp() public { deal(DAI, address(this), 1000 * 1e18); dai.approve(address(router), type(uint256).max); } // - Swap 1000 DAI for WETH on DAI/WETH pool with 0.3% fee // - Send WETH from Uniswap V3 to this contract function test_exactInputSingle() public { uint256 wethBefore = weth.balanceOf(address(this)); // Call router.exactInputSingle ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ tokenIn: DAI, tokenOut: WETH, fee: POOL_FEE, recipient: address(this), amountIn: 1000 * 1e18, amountOutMinimum: 0, sqrtPriceLimitX96: 0 }); uint256 amountOut = router.exactInputSingle(params); uint256 wethAfter = weth.balanceOf(address(this)); console2.log("WETH amount out %e", amountOut); assertGt(amountOut, 0); assertEq(wethAfter - wethBefore, amountOut); } // Swap 1000 DAI for WETH and then WETH to WBTC // - Swap DAI to WETH on pool with 0.3% fee // - Swap WETH to WBTC on pool with 0.3% fee // - Send WBTC from Uniswap V3 to this contract // NOTE: WBTC has 8 decimals function test_exactInput() public { // Call router.exactInput // DAI->WETH->WBTC bytes memory path = abi.encodePacked(DAI, POOL_FEE, WETH, POOL_FEE, WBTC); ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ path: path, recipient: address(this), amountIn: 1000 * 1e18, amountOutMinimum: 0 }); uint256 amountOut = router.exactInput(params); console2.log("WBTC amount out %e", amountOut); assertGt(amountOut, 0); assertEq(wbtc.balanceOf(address(this)), amountOut); } // - Swap maximum of 1000 DAI to obtain exactly 0.1 WETH from DAI/WETH pool with 0.3% fee // - Send WETH from Uniswap V3 to this contract function test_exactOutputSingle() public { uint256 wethBefore = weth.balanceOf(address(this)); ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams({ tokenIn: DAI, tokenOut: WETH, fee: POOL_FEE, recipient: address(this), amountOut: 0.1 * 1e18, amountInMaximum: 1000 * 1e18, sqrtPriceLimitX96: 0 }); uint256 amountIn = router.exactOutputSingle(params); uint256 wethAfter = weth.balanceOf(address(this)); console2.log("DAI amount in %e", amountIn); assertLe(amountIn, 1000 * 1e18); assertEq(wethAfter - wethBefore, 0.1 * 1e18); } // Swap maximum of 1000 DAI to obtain exactly 0.01 WBTC // - Swap DAI to WETH on pool with 0.3% fee // - Swap WETH to WBTC on pool with 0.3% fee // - Send WBTC from Uniswap V3 to this contract // NOTE: WBTC has 8 decimals function test_exactOutput() public { // Call router.exactOutput bytes memory path = abi.encodePacked(WBTC, POOL_FEE, WETH, POOL_FEE, DAI); ISwapRouter.ExactOutputParams memory params = ISwapRouter.ExactOutputParams({ path: path, recipient: address(this), amountOut: 0.01 * 1e18, amountInMaximum: 1000 * 1e18 }); uint256 amountIn = router.exactOutput(params); console2.log("DAI amount in %e", amountIn); assertLe(amountIn, 1000 * 1e18); assertEq(wbtc.balanceOf(address(this)), 0.01 * 1e8); } }
2025年08月16日
7 阅读
0 评论
1 点赞
2025-08-10
Uniswap v2-Flash Swap-Twap-Arbitrage
一、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); } }
2025年08月10日
3 阅读
0 评论
1 点赞
2025-08-05
Uniswap v2-Liquidity
一、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"); } }
2025年08月05日
1 阅读
0 评论
1 点赞
2025-08-02
Uniswap v2-Swap
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"); } }
2025年08月02日
1 阅读
0 评论
1 点赞
1
...
4
5
6