首页
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
页面
搜索到
3
篇与
的结果
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-19
Uniswap V3-Prat II
一、factory二、LiquidityPoolmint用户调用NonfungiblePositionManager 的 mint 来调用 Pool 的 mint,获取到如果要增加 amount数量的流动性到 pool需要添加多少的 token0 和 token1function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external override lock returns (uint256 amount0, uint256 amount1) { require(amount > 0); (, int256 amount0Int, int256 amount1Int) = _modifyPosition( ModifyPositionParams({ owner: recipient, tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: int256(amount).toInt128() }) ); amount0 = uint256(amount0Int); amount1 = uint256(amount1Int); uint256 balance0Before; uint256 balance1Before; if (amount0 > 0) balance0Before = balance0(); if (amount1 > 0) balance1Before = balance1(); IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data); if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0'); if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1'); emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1); }调用 _modifyPosition根据当前价格和提供的区间,算出需要的 token0 / token1 数量。更新 position 信息。IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data)回调给调用方(通常是 Manager),让它把 token0/1 转进池子。回调设计是 Uniswap 的一大特点,保证池子不用持有别人的授权。校验资金到位。触发 Mint 事件。function _modifyPosition(ModifyPositionParams memory params) private noDelegateCall returns ( Position.Info storage position, int256 amount0, int256 amount1 ) { checkTicks(params.tickLower, params.tickUpper); Slot0 memory _slot0 = slot0; // SLOAD for gas optimization position = _updatePosition( params.owner, params.tickLower, params.tickUpper, params.liquidityDelta, _slot0.tick ); if (params.liquidityDelta != 0) { if (_slot0.tick < params.tickLower) { // current tick is below the passed range; liquidity can only become in range by crossing from left to // right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it amount0 = SqrtPriceMath.getAmount0Delta( TickMath.getSqrtRatioAtTick(params.tickLower), TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta ); } else if (_slot0.tick < params.tickUpper) { // current tick is inside the passed range uint128 liquidityBefore = liquidity; // SLOAD for gas optimization // write an oracle entry (slot0.observationIndex, slot0.observationCardinality) = observations.write( _slot0.observationIndex, _blockTimestamp(), _slot0.tick, liquidityBefore, _slot0.observationCardinality, _slot0.observationCardinalityNext ); amount0 = SqrtPriceMath.getAmount0Delta( _slot0.sqrtPriceX96, TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta ); amount1 = SqrtPriceMath.getAmount1Delta( TickMath.getSqrtRatioAtTick(params.tickLower), _slot0.sqrtPriceX96, params.liquidityDelta ); liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta); } else { // current tick is above the passed range; liquidity can only become in range by crossing from right to // left, when we'll need _more_ token1 (it's becoming more valuable) so user must provide it amount1 = SqrtPriceMath.getAmount1Delta( TickMath.getSqrtRatioAtTick(params.tickLower), TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta ); } } }核心逻辑:根据当前价格 slot0.tick 和 [tickLower, tickUpper] 的关系,决定需要多少 token0 和 token1。如果 当前价格 < tickLower→ 价格在区间左边→ LP 的流动性还没生效(要等价格涨到区间才会生效)。→ 用户只需要提供 token0。如果 tickLower <= 当前价格 < tickUpper→ 价格在区间内→ 流动性马上生效。→ 用户需要同时提供 token0 + token1。如果 当前价格 >= tickUpper→ 价格在区间右边→ 流动性还没生效(要等价格跌到区间才会生效)。→ 用户只需要提供 token1。🔹 _updatePosition的主要流程当用户调用 mint()(增加流动性)或者 burn()(减少流动性)时,都会进入 _updatePosition,它要做的事可以总结为三步:1.取出当前 position 信息position = positions.get(owner, tickLower, tickUpper);每个 LP 的区间 [tickLower, tickUpper] 有独立的 position,记录了流动性和手续费信息。2.更新边界 tick(tickLower 和 tickUpper)flippedLower = ticks.update(..., false, ...); flippedUpper = ticks.update(..., true, ...);tick.update 会调整每个 tick 的流动性(gross 和 net),并判断这个 tick 是否被“翻转”了(从无流动性变成有流动性,或者相反)。如果翻转了,tickBitmap 会 flip,表示这个 tick 现在是“活跃的边界”,swap 的时候可能会跨到它。3.更新 position 里存储的 feeGrowth 累计值(feeGrowthInside0X128, feeGrowthInside1X128) = ticks.getFeeGrowthInside(...); position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);这一步是为了让 LP 将来能精确领取手续费(因为手续费是按“进入 position 的时刻”的增长率开始算的)。🔹 ticks.update 内部逻辑tick 本质上就是“流动性的边界点”,每个 tick 记录了 跨过它时要加/减多少流动性。uint128 liquidityGrossBefore = info.liquidityGross; uint128 liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta);liquidityGross:这个 tick 上总共有多少流动性挂靠。liquidityNet:当价格跨过这个 tick时,池子总流动性要加还是减。再结合 upper 参数:如果是 下边界(lower tick):进入区间时要加 liquidity,出去时要减。如果是 上边界(upper tick):进入区间时要减 liquidity,出去时要加。所以 liquidityNet 更新时符号是反的:info.liquidityNet = upper ? int256(info.liquidityNet).sub(liquidityDelta).toInt128() : int256(info.liquidityNet).add(liquidityDelta).toInt128();burn function burn( int24 tickLower, int24 tickUpper, uint128 amount ) external override lock returns (uint256 amount0, uint256 amount1) { (Position.Info storage position, int256 amount0Int, int256 amount1Int) = _modifyPosition( ModifyPositionParams({ owner: msg.sender, tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: -int256(amount).toInt128() }) ); amount0 = uint256(-amount0Int); amount1 = uint256(-amount1Int); if (amount0 > 0 || amount1 > 0) { (position.tokensOwed0, position.tokensOwed1) = ( position.tokensOwed0 + uint128(amount0), position.tokensOwed1 + uint128(amount1) ); } emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1); }作用:用户调用 NonfungiblePositionManager 的 burn 来调用 Pool 的 burn 从一个指定的 price range [tickLower, tickUpper] 中移除流动性。输入参数:tickLower:这个 position 的下边界 ticktickUpper:这个 position 的上边界 tickamount:要移除多少流动性 (liquidity)返回值:amount0:用户从这次 burn 对应的 position 上应得的 token0 数量amount1:用户从这次 burn 对应的 position 上应得的 token1 数量⚠️ 注意:调用 burn 后,用户 并不会立刻拿到 token0 / token1,而是把它们记录到 position 的 tokensOwed0 / tokensOwed1 中。用户要再调用 collect() 才能真正提走代币。核心逻辑调用_modifyPosition(Position.Info storage position, int256 amount0Int, int256 amount1Int) = _modifyPosition( ModifyPositionParams({ owner: msg.sender, tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: -int256(amount).toInt128() }) );_modifyPosition 是内部函数,负责更新 position 的状态。liquidityDelta 设置为 负数,表示减少流动性。它会返回:position:用户在这个区间内的 position 结构体amount0Int:对应减少这部分流动性时,position 内减少的 token0 数量(可能是负数)amount1Int:对应减少这部分流动性时,position 内减少的 token1 数量取正数作为返回值amount0 = uint256(-amount0Int); amount1 = uint256(-amount1Int);这里 amount0Int、amount1Int 可能是负数,所以取反再转成正数。amount0 / amount1 = 本次 burn 这部分流动性应释放出来的 token0、token1 数量。累加到 position 的应收款项if (amount0 > 0 || amount1 > 0) { (position.tokensOwed0, position.tokensOwed1) = ( position.tokensOwed0 + uint128(amount0), position.tokensOwed1 + uint128(amount1) ); }用户移除了流动性,但 Uniswap 不会马上转钱给你,而是把应得的 token0/token1 加到 tokensOwed0/tokensOwed1。等你再调用 collect() 时,这些累计的 owed token 会真正打给你。实际打钱时用的函数collectfunction collect( address recipient, int24 tickLower, int24 tickUpper, uint128 amount0Requested, uint128 amount1Requested ) external override lock returns (uint128 amount0, uint128 amount1) { // we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1} Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper); amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested; amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested; if (amount0 > 0) { position.tokensOwed0 -= amount0; TransferHelper.safeTransfer(token0, recipient, amount0); } if (amount1 > 0) { position.tokensOwed1 -= amount1; TransferHelper.safeTransfer(token1, recipient, amount1); } emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1); }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 {IWETH} from "../src/interfaces/IWETH.sol"; import {INonfungiblePositionManager} from "../src/interfaces/uniswap-v3/INonfungiblePositionManager.sol"; import {UNISWAP_V3_NONFUNGIBLE_POSITION_MANAGER, DAI, WETH} from "../src/Constants.sol"; struct Position { uint96 nonce; address operator; address token0; address token1; uint24 fee; int24 tickLower; int24 tickUpper; uint128 liquidity; uint256 feeGrowthInside0LastX128; uint256 feeGrowthInside1LastX128; uint128 tokensOwed0; uint128 tokensOwed1; } contract UniswapV3LiquidityTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); INonfungiblePositionManager private constant manager = INonfungiblePositionManager(UNISWAP_V3_NONFUNGIBLE_POSITION_MANAGER); // 0.3% int24 private constant MIN_TICK = -887272; int24 private constant MAX_TICK = 887272; // DAI/WETH 3000 uint24 private constant POOL_FEE = 3000; int24 private constant TICK_SPACING = 60; function setUp() public { deal(DAI, address(this), 3000 * 1e18); deal(WETH, address(this), 3 * 1e18); weth.approve(address(manager), type(uint256).max); dai.approve(address(manager), type(uint256).max); } function mint() private returns (uint256 tokenId) { (tokenId,,,) = manager.mint( INonfungiblePositionManager.MintParams({ token0: DAI, token1: WETH, fee: POOL_FEE, tickLower: MIN_TICK / TICK_SPACING * TICK_SPACING, tickUpper: MAX_TICK / TICK_SPACING * TICK_SPACING, amount0Desired: 1000 * 1e18, amount1Desired: 1e18, amount0Min: 0, amount1Min: 0, recipient: address(this), deadline: block.timestamp }) ); } function getPosition(uint256 tokenId) private view returns (Position memory) { ( uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1 ) = manager.positions(tokenId); Position memory position = Position({ nonce: nonce, operator: operator, token0: token0, token1: token1, fee: fee, tickLower: tickLower, tickUpper: tickUpper, liquidity: liquidity, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128, tokensOwed0: tokensOwed0, tokensOwed1: tokensOwed1 }); return position; } // Mint a new position by adding liquidity to DAI/WETH pool with 0.3% fee. // - You are free to choose the price range // - Ticks must be divisible by tick spacing of the pool // - This test contract is given 3000 DAI and 3 WETH. Put any amount of tokens // not exceeding this contracts's balance. // - Set recipient of NFT (that represents the ownership of this position) to this contract. function test_mint() public { (uint256 tokenId,, uint256 amount0, uint256 amount1) = manager.mint( INonfungiblePositionManager.MintParams({ token0: DAI, token1: WETH, fee: POOL_FEE, tickLower: MIN_TICK / TICK_SPACING * TICK_SPACING, tickUpper: MAX_TICK / TICK_SPACING * TICK_SPACING, amount0Desired: 1000 * 1e18, amount1Desired: 1e18, amount0Min: 0, amount1Min: 0, recipient: address(this), deadline: block.timestamp }) ); console2.log("Amount 0 added %e", amount0); console2.log("Amount 1 added %e", amount1); assertEq(manager.ownerOf(tokenId), address(this)); Position memory position = getPosition(tokenId); assertEq(position.token0, DAI); assertEq(position.token1, WETH); assertGt(position.liquidity, 0); } // Increase liquidity for the position with token id = `tokenId`. // 3000 DAI and 3 WETH were initially given to this contract. // Some of the tokens where used to mint a new position. // Use any token amount less than or equal to contract's balance. function test_increaseLiquidity() public { uint256 tokenId = mint(); Position memory p0 = getPosition(tokenId); (uint256 liquidityDelta, uint256 amount0, uint256 amount1) = manager.increaseLiquidity( INonfungiblePositionManager.IncreaseLiquidityParams({ tokenId: tokenId, amount0Desired: 1000 * 1e18, amount1Desired: 1e18, amount0Min: 0, amount1Min: 0, deadline: block.timestamp }) ); console2.log("Amount 0 added %e", amount0); console2.log("Amount 1 added %e", amount1); Position memory p1 = getPosition(tokenId); assertGt(p1.liquidity, p0.liquidity); assertGt(liquidityDelta, 0); } // Decrease liquidity for the position with token id = `tokenId`. // - Amount of liquidity to decrease cannot exceed the position's liquidity. function test_decreaseLiquidity() public { uint256 tokenId = mint(); Position memory p0 = getPosition(tokenId); (uint256 amount0, uint256 amount1) = manager.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams({ tokenId: tokenId, liquidity: p0.liquidity, amount0Min: 0, amount1Min: 0, deadline: block.timestamp }) ); console2.log("Amount 0 decreased %e", amount0); console2.log("Amount 1 decreased %e", amount1); Position memory p1 = getPosition(tokenId); assertEq(p1.liquidity, 0); assertGt(p1.tokensOwed0, 0); assertGt(p1.tokensOwed1, 0); } // Remove all liquidity (including fees) from a position by calling collect() // - Decrease all liquidity for the position with token id = `tokenId` // - Transfer tokens from NonFungiblePositionManager to this contract // by calling collect() function test_collect() public { uint256 tokenId = mint(); Position memory p0 = getPosition(tokenId); manager.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams({ tokenId: tokenId, liquidity: p0.liquidity, amount0Min: 0, amount1Min: 0, deadline: block.timestamp }) ); (uint256 amount0, uint256 amount1) = manager.collect( INonfungiblePositionManager.CollectParams({ tokenId: tokenId, recipient: address(this), amount0Max: type(uint128).max, amount1Max: type(uint128).max }) ); console2.log("--- collect ---"); console2.log("Amount 0 collected %e", amount0); console2.log("Amount 1 collected %e", amount1); Position memory p1 = getPosition(tokenId); assertEq(p1.liquidity, 0); assertEq(p1.tokensOwed0, 0); assertEq(p1.tokensOwed1, 0); } }
2025年08月19日
10 阅读
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 点赞