首页
Search
1
yamux: how to work?
79 阅读
2
The Art of Memory Allocation: Malloc, Slab, C++ STL, and GoLang Memory Allocation
71 阅读
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
页面
搜索到
34
篇与
的结果
2025-08-26
Damn-vulnerable-defi-V4-solution(Unstoppable)
一、Unstoppable// UnstoppableMonitor.sol // SPDX-License-Identifier: MIT // Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz) pragma solidity =0.8.25; import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import {Owned} from "solmate/auth/Owned.sol"; import {UnstoppableVault, ERC20} from "../unstoppable/UnstoppableVault.sol"; /** * @notice Permissioned contract for on-chain monitoring of the vault's flashloan feature. */ contract UnstoppableMonitor is Owned, IERC3156FlashBorrower { UnstoppableVault private immutable vault; error UnexpectedFlashLoan(); event FlashLoanStatus(bool success); constructor(address _vault) Owned(msg.sender) { vault = UnstoppableVault(_vault); } function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata) external returns (bytes32) { if (initiator != address(this) || msg.sender != address(vault) || token != address(vault.asset()) || fee != 0) { revert UnexpectedFlashLoan(); } ERC20(token).approve(address(vault), amount); return keccak256("IERC3156FlashBorrower.onFlashLoan"); } function checkFlashLoan(uint256 amount) external onlyOwner { require(amount > 0); address asset = address(vault.asset()); try vault.flashLoan(this, asset, amount, bytes("")) { emit FlashLoanStatus(true); } catch { // Something bad happened emit FlashLoanStatus(false); // Pause the vault vault.setPause(true); // Transfer ownership to allow review & fixes vault.transferOwnership(owner); } } } // UnstoppableVault.sol // SPDX-License-Identifier: MIT // Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz) pragma solidity =0.8.25; import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {Owned} from "solmate/auth/Owned.sol"; import {SafeTransferLib, ERC4626, ERC20} from "solmate/tokens/ERC4626.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {IERC3156FlashBorrower, IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156.sol"; /** * An ERC4626-compliant tokenized vault offering flashloans for a fee. * An owner can pause the contract and execute arbitrary changes. */ contract UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC4626, Pausable { using SafeTransferLib for ERC20; using FixedPointMathLib for uint256; uint256 public constant FEE_FACTOR = 0.05 ether; uint64 public constant GRACE_PERIOD = 30 days; uint64 public immutable end = uint64(block.timestamp) + GRACE_PERIOD; address public feeRecipient; error InvalidAmount(uint256 amount); error InvalidBalance(); error CallbackFailed(); error UnsupportedCurrency(); event FeeRecipientUpdated(address indexed newFeeRecipient); constructor(ERC20 _token, address _owner, address _feeRecipient) ERC4626(_token, "Too Damn Valuable Token", "tDVT") Owned(_owner) { feeRecipient = _feeRecipient; emit FeeRecipientUpdated(_feeRecipient); } /** * @inheritdoc IERC3156FlashLender */ function maxFlashLoan(address _token) public view nonReadReentrant returns (uint256) { if (address(asset) != _token) { return 0; } return totalAssets(); } /** * @inheritdoc IERC3156FlashLender */ function flashFee(address _token, uint256 _amount) public view returns (uint256 fee) { if (address(asset) != _token) { revert UnsupportedCurrency(); } if (block.timestamp < end && _amount < maxFlashLoan(_token)) { return 0; } else { return _amount.mulWadUp(FEE_FACTOR); } } /** * @inheritdoc ERC4626 */ function totalAssets() public view override nonReadReentrant returns (uint256) { return asset.balanceOf(address(this)); } /** * @inheritdoc IERC3156FlashLender */ function flashLoan(IERC3156FlashBorrower receiver, address _token, uint256 amount, bytes calldata data) external returns (bool) { if (amount == 0) revert InvalidAmount(0); // fail early if (address(asset) != _token) revert UnsupportedCurrency(); // enforce ERC3156 requirement uint256 balanceBefore = totalAssets(); if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement // transfer tokens out + execute callback on receiver ERC20(_token).safeTransfer(address(receiver), amount); // callback must return magic value, otherwise assume it failed uint256 fee = flashFee(_token, amount); if ( receiver.onFlashLoan(msg.sender, address(asset), amount, fee, data) != keccak256("IERC3156FlashBorrower.onFlashLoan") ) { revert CallbackFailed(); } // pull amount + fee from receiver, then pay the fee to the recipient ERC20(_token).safeTransferFrom(address(receiver), address(this), amount + fee); ERC20(_token).safeTransfer(feeRecipient, fee); return true; } /** * @inheritdoc ERC4626 */ function beforeWithdraw(uint256 assets, uint256 shares) internal override nonReentrant {} /** * @inheritdoc ERC4626 */ function afterDeposit(uint256 assets, uint256 shares) internal override nonReentrant whenNotPaused {} function setFeeRecipient(address _feeRecipient) external onlyOwner { if (_feeRecipient != address(this)) { feeRecipient = _feeRecipient; emit FeeRecipientUpdated(_feeRecipient); } } // Allow owner to execute arbitrary changes when paused function execute(address target, bytes memory data) external onlyOwner whenPaused { (bool success,) = target.delegatecall(data); require(success); } // Allow owner pausing/unpausing this contract function setPause(bool flag) external onlyOwner { if (flag) _pause(); else _unpause(); } } 为了让 monitor 将 vault 的状态修改为 stop,需要在monitor 调用 flashLoan 的时候触发 revert,flashLoan 中要触发 revert,比较好触发的就是这行if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();这块代码有一些问题,这个检查试图确保Vault的资产余额与份额总供应量转换后的值一致,但因为它比较的是两种不同的单位(份额和资产数量),而且当有人直接向Vault转账token时(而不是通过deposit函数),资产余额会增加但份额不变,导致检查失败,从而使flashLoan功能被禁用。修复之后/** * @inheritdoc IERC3156FlashLender */ function flashLoan(IERC3156FlashBorrower receiver, address _token, uint256 amount, bytes calldata data) external returns (bool) { if (amount == 0) revert InvalidAmount(0); // fail early if (address(asset) != _token) revert UnsupportedCurrency(); // enforce ERC3156 requirement uint256 balanceBefore = totalAssets(); // 注释了这里的检查 // if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement // transfer tokens out + execute callback on receiver ERC20(_token).safeTransfer(address(receiver), amount); // callback must return magic value, otherwise assume it failed uint256 fee = flashFee(_token, amount); if ( receiver.onFlashLoan(msg.sender, address(asset), amount, fee, data) != keccak256("IERC3156FlashBorrower.onFlashLoan") ) { revert CallbackFailed(); } // pull amount + fee from receiver, then pay the fee to the recipient ERC20(_token).safeTransferFrom(address(receiver), address(this), amount + fee); ERC20(_token).safeTransfer(feeRecipient, fee); // 此处增加了借贷之前和之后的资产余额比较 确保Vault的资产余额没有减少 if (totalAssets() < balanceBefore) revert InvalidBalance(); return true; }
2025年08月26日
26 阅读
0 评论
1 点赞
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 点赞
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 点赞
1
...
4
5
6
7