首页
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
页面
搜索到
2
篇与
的结果
2025-08-05
Uniswap v2-Liquidity
一、Create pool1.createPair创建流动性时,需要确保交易对存在,如果不存在将会调用IUniswapV2Factory(factory).createPair创建交易对// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol function _addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin ) internal virtual returns (uint amountA, uint amountB) { // create the pair if it doesn't exist yet if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { IUniswapV2Factory(factory).createPair(tokenA, tokenB); } .... }这个函数是 UniswapV2Factory 的核心方法,用于动态创建新的代币交易对合约。// Uniswap/v2-core/contracts/UniswapV2Factory.sol function createPair(address tokenA, address tokenB) external returns (address pair) { require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient bytes memory bytecode = type(UniswapV2Pair).creationCode; bytes32 salt = keccak256(abi.encodePacked(token0, token1)); assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) } IUniswapV2Pair(pair).initialize(token0, token1); getPair[token0][token1] = pair; getPair[token1][token0] = pair; // populate mapping in the reverse direction allPairs.push(pair); emit PairCreated(token0, token1, pair, allPairs.length); }参数校验require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); (address token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');禁止相同代币:防止创建无意义的交易对(如 ETH/ETH)。代币排序:按地址升序排列 token0 和 token1,确保唯一性(避免重复创建 ETH/USDT 和 USDT/ETH)。零地址检查:防止无效代币。重复创建检查:确保该交易对尚未存在。使用 CREATE2 部署合约bytes memory bytecode = type(UniswapV2Pair).creationCode; bytes32 salt = keccak256(abi.encodePacked(token0, token1)); assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) }CREATE2 特性:通过 salt(代币对哈希)确定合约地址,地址可预测且与部署顺序无关。公式:address = hash(0xFF, factory, hash(token0, token1), init_code_hash)。操作步骤:获取 UniswapV2Pair 合约的初始化字节码(creationCode)。用 token0 和 token1 生成唯一的 salt。内联汇编调用 create2,返回新合约地址 pair。当 Uniswap V2 开发时(2020 年),Solidity 尚未内置 new 关键字支持 CREATE2(直到 Solidity 0.8+ 才原生支持)。必须通过汇编直接调用 EVM 的 CREATE2 操作码。参数解释:0:不发送 Ether(value)。add(bytecode, 32):跳过字节码的前 32 字节(长度字段),指向实际代码。mload(bytecode):读取字节码长度。salt:基于代币对的唯一标识(keccak256(token0, token1))。初始化交易对IUniswapV2Pair(pair).initialize(token0, token1);调用新 Pair 合约的 initialize 方法,设置代币地址 token0 和 token1。为什么单独初始化?分离部署和初始化,避免构造函数参数传递复杂性。确保代币顺序与 Factory 一致。更新工厂状态getPair[token0][token1] = pair; getPair[token1][token0] = pair; // 反向映射 allPairs.push(pair); emit PairCreated(token0, token1, pair, allPairs.length);记录交易对:双向映射 getPair,支持通过任意代币顺序查询。allPairs 数组保存所有交易对地址,便于遍历。事件日志:通知监听者新交易对创建。分叉主网测试createPairfunction test_createPair() public { ERC20 token = new ERC20("test", "TEST", 18); address pair = factory.createPair(address(token), address(weth)); address token0 = IUniswapV2Pair(pair).token0(); address token1 = IUniswapV2Pair(pair).token1(); if (address(token) < WETH) { assertEq(token0, address(token), "token 0"); assertEq(token1, WETH, "token 1"); } else { assertEq(token0, WETH, "token 0"); assertEq(token1, address(token), "token 1"); } }二、add liquidity// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol function addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); liquidity = IUniswapV2Pair(pair).mint(to); }该函数允许用户向 Uniswap V2 交易对(如 ETH/USDT)添加流动性,并获取对应的 LP 代币(流动性凭证)。主要步骤包括:计算最优添加量、代币转账、铸造 LP 代币。参数说明参数类型作用tokenAaddress代币 A 的地址tokenBaddress代币 B 的地址amountADesireduint用户希望添加的 A 代币数量amountBDesireduint用户希望添加的 B 代币数量amountAMinuint用户可接受的最少 A 代币实际添加量(防滑点)amountBMinuint用户可接受的最少 B 代币实际添加量(防滑点)toaddress接收 LP 代币的地址deadlineuint交易过期时间(Unix 时间戳)代码逻辑分步解析1. 修饰符 ensure(deadline)modifier ensure(uint deadline) { require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); _; }作用:确保交易在 deadline 之前被执行,否则回滚(防止过期交易被意外打包)。2. 计算实际添加量 _addLiquidity(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);内部函数 _addLiquidity 会:检查交易对是否存在(若不存在则自动创建)。根据当前池子比例计算最优的 amountA 和 amountB(避免大幅改变价格)。验证 amountA ≥ amountAMin 和 amountB ≥ amountBMin(防止高滑点损失)。3. 获取交易对地址address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);pairFor:通过工厂合约和代币地址计算 Pair 合约地址(使用 CREATE2 确定性地址)。例如:tokenA = WETH, tokenB = USDT → 返回 WETH/USDT 交易对地址。4. 代币转账到交易对TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);safeTransferFrom:安全地将代币从用户钱包转入交易对合约。需要用户提前授权(approve)给 Router 合约。若转账失败(如余额不足),会回滚交易。5. 铸造 LP 代币liquidity = IUniswapV2Pair(pair).mint(to);mint:调用 Pair 合约的铸造函数:根据添加的流动性比例,计算应铸造的 LP 代币数量。将 LP 代币发送给 to 地址。返回铸造的 LP 数量(liquidity)。// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol function _addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin ) internal virtual returns (uint amountA, uint amountB) { // create the pair if it doesn't exist yet if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { IUniswapV2Factory(factory).createPair(tokenA, tokenB); } (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); if (reserveA == 0 && reserveB == 0) { (amountA, amountB) = (amountADesired, amountBDesired); } else { uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); if (amountBOptimal <= amountBDesired) { require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); (amountA, amountB) = (amountADesired, amountBOptimal); } else { uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); assert(amountAOptimal <= amountADesired); require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); (amountA, amountB) = (amountAOptimal, amountBDesired); } } }该函数是 Uniswap V2 添加流动性的核心内部逻辑,负责:自动创建交易对(如果不存在)。按最优比例计算实际添加量(防止改变市场价格)。验证滑点限制(amountAMin 和 amountBMin)。代码逻辑分步解析1. 创建交易对(如果不存在)if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { IUniswapV2Factory(factory).createPair(tokenA, tokenB); }作用:检查代币对是否存在,若不存在则通过工厂合约创建。关键点:使用 CREATE2 确定性地址,确保同一代币对在不同网络的地址一致。新创建的池子初始储备金为 0。2. 获取当前储备金(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);getReserves:从 Pair 合约中读取 reserve0 和 reserve1(已按代币排序)。3. 处理空池情况if (reserveA == 0 && reserveB == 0) { (amountA, amountB) = (amountADesired, amountBDesired); }逻辑:若池子为空,直接接受用户提供的全部代币量(成为初始流动性提供者)。示例:用户首次添加 1 ETH + 2000 USDT → 池子比例设为 1:2000。4. 非空池的比例计算uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); if (amountBOptimal <= amountBDesired) { require(amountBOptimal >= amountBMin, 'INSUFFICIENT_B_AMOUNT'); (amountA, amountB) = (amountADesired, amountBOptimal); } else { uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); assert(amountAOptimal <= amountADesired); require(amountAOptimal >= amountAMin, 'INSUFFICIENT_A_AMOUNT'); (amountA, amountB) = (amountAOptimal, amountBDesired); }(1) 计算最优比例quote 函数:根据当前池子比例计算匹配量。// 公式:amountBOptimal = (amountADesired * reserveB) / reserveA两种场景:Case 1:用户提供的 amountBDesired 足够匹配 amountADesired(amountBOptimal ≤ amountBDesired)→ 使用 amountADesired 和 amountBOptimal。Case 2:用户提供的 amountBDesired 不足 → 反向计算 amountAOptimal。(2) 滑点验证require(amountBOptimal >= amountBMin):确保实际添加的 amountB 不低于用户设置的最小值(防止高滑点损失)。assert(amountAOptimal <= amountADesired):内部安全检查(应恒成立,否则代码有误)。// Uniswap/v2-core/contracts/UniswapV2Pair.sol function mint(address to) external lock returns (uint liquidity) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings uint balance0 = IERC20(token0).balanceOf(address(this)); uint balance1 = IERC20(token1).balanceOf(address(this)); uint amount0 = balance0.sub(_reserve0); uint amount1 = balance1.sub(_reserve1); bool feeOn = _mintFee(_reserve0, _reserve1); uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee if (_totalSupply == 0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); } require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); _mint(to, liquidity); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Mint(msg.sender, amount0, amount1); }mint 是 UniswapV2 Pair 合约的核心函数,负责向流动性池添加流动性并铸造对应的 LP 代币。调用者为 Router 合约,用户通过 Router 间接调用。关键步骤解析1. 获取储备金和当前余额(uint112 _reserve0, uint112 _reserve1,) = getReserves(); uint balance0 = IERC20(token0).balanceOf(address(this)); uint balance1 = IERC20(token1).balanceOf(address(this)); uint amount0 = balance0.sub(_reserve0); // 新增的 token0 数量 uint amount1 = balance1.sub(_reserve1); // 新增的 token1 数量作用:计算用户实际转入的代币量(amount0 和 amount1)。注意:getReserves() 返回的是上一次调用 _update 时的值,而 balanceOf 是实时余额。2. 手续费处理(_mintFee)bool feeOn = _mintFee(_reserve0, _reserve1);逻辑:如果协议手续费开启(默认 0.05% 给工厂合约),检查是否需要铸造手续费对应的 LP 代币。手续费计算基于 kLast(上次手续费结算时的 reserve0 * reserve1)与当前储备金的差值。目的:确保流动性提供者(LP)在提取流动性时支付应得的手续费。3. 计算应铸造的 LP 数量情况1:首次添加流动性(_totalSupply == 0)liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); // 永久锁定初始流动性MINIMUM_LIQUIDITY(默认 1000 wei)被永久锁定,防止首次添加时 LP 代币被恶意操纵。初始流动性决定了 LP 代币的总供应基准。情况2:非首次添加liquidity = Math.min( amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1 );按代币添加量的较小比例铸造 LP,确保新增流动性不改变当前价格比例。4. 校验与铸造 LP 代币require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); _mint(to, liquidity); // 给用户地址铸造 LP 代币防呆检查:防止零流动性铸造。ERC20 操作:通过 _mint 将 LP 代币发送给用户。5. 更新储备金和触发事件_update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); emit Mint(msg.sender, amount0, amount1);_update:更新储备金为最新余额,并计算累计价格。kLast:记录当前储备金乘积,用于下次手续费计算。事件:通知外部流动性添加详情。分叉主网测试addLiquiditycontract UniswapV2LiquidityTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_WETH); address private constant user = address(100); function setUp() public { // Fund WETH to user deal(user, 100 * 1e18); vm.startPrank(user); weth.deposit{value: 100 * 1e18}(); weth.approve(address(router), type(uint256).max); vm.stopPrank(); // Fund DAI to user deal(DAI, user, 1000000 * 1e18); vm.startPrank(user); dai.approve(address(router), type(uint256).max); vm.stopPrank(); } function test_addLiquidity() public { vm.prank(user); (,, uint256 liquidity) = router.addLiquidity(WETH, DAI, 100 * 1e18, 1000000 * 1e18, 1, 1, user, block.timestamp); console2.log("LP:", liquidity); assertGt(pair.balanceOf(user), 0, "LP = 0"); assertEq(pair.balanceOf(user), liquidity, "user LP = liquidity"); } }三、Remove liquidity// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol function removeLiquidity( address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) { address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); }该函数是 UniswapV2 Router 合约的核心方法,允许用户销毁 LP 代币并提取对应比例的两种底层代币。主要步骤包括:转移 LP 代币、销毁 LP 计算赎回量、滑点验证和代币转账。参数说明参数类型作用tokenA, tokenBaddress流动池中的两种代币地址liquidityuint要销毁的 LP 代币数量amountAMin, amountBMinuint用户能接受的最少提取量(滑点保护)toaddress接收提取代币的地址deadlineuint交易过期时间执行流程分步解析1. 权限与有效期检查ensure(deadline) // 修饰器检查交易未过期防止用户签名交易被延迟执行后因价格波动造成损失。2. 获取交易对地址address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);通过工厂合约和代币地址计算 Pair 合约的确定性地址(CREATE2 生成)。3. 转移 LP 代币到交易对IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity);前置条件:用户需提前 approve Router 合约操作其 LP 代币。将 LP 代币从用户转到 Pair 合约,准备销毁。4. 销毁 LP 并计算赎回量(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);burn 函数内部逻辑:根据 LP 占总供应量的比例,计算应返还的代币量:将 amount0 和 amount1 转给 to 地址。销毁 LP 代币,更新储备金。5. 代币排序与金额映射(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);将 Pair 合约返回的 amount0/amount1 按用户输入的 tokenA/tokenB 顺序重新映射,确保接口友好性。6. 滑点验证require(amountA >= amountAMin, 'INSUFFICIENT_A_AMOUNT'); require(amountB >= amountBMin, 'INSUFFICIENT_B_AMOUNT');防止因价格波动或抢跑攻击导致用户提取量过少。例如:用户设置 amountAMin = 0.9 ETH,若实际仅提取 0.8 ETH,交易回滚。// Uniswap/v2-core/contracts/UniswapV2Pair.sol function burn(address to) external lock returns (uint amount0, uint amount1) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings address _token0 = token0; // gas savings address _token1 = token1; // gas savings uint balance0 = IERC20(_token0).balanceOf(address(this)); uint balance1 = IERC20(_token1).balanceOf(address(this)); uint liquidity = balanceOf[address(this)]; bool feeOn = _mintFee(_reserve0, _reserve1); uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); _burn(address(this), liquidity); _safeTransfer(_token0, to, amount0); _safeTransfer(_token1, to, amount1); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Burn(msg.sender, amount0, amount1, to); }burn 是 UniswapV2 Pair 合约的关键函数,负责销毁 LP 代币并返还对应比例的底层代币。它是流动性移除的底层实现,由 Router 合约调用。执行流程与关键机制1. 状态读取与缓存(Gas 优化)(uint112 _reserve0, uint112 _reserve1,) = getReserves(); address _token0 = token0; // 代币0地址缓存 address _token1 = token1; // 代币1地址缓存 uint balance0 = IERC20(_token0).balanceOf(address(this)); uint balance1 = IERC20(_token1).balanceOf(address(this)); uint liquidity = balanceOf[address(this)]; // 待销毁的LP数量(用户转到 pair 合约)目的:减少链上读取次数(_reserve0、_token0等只读一次),节省 Gas。注意:balance0 和 balance1 是当前合约的实际余额,可能包含未计入储备金的代币(如手续费)。2. 手续费处理(_mintFee)bool feeOn = _mintFee(_reserve0, _reserve1);逻辑:若协议手续费开启(0.05%),检查 kLast(上次手续费结算时的 reserve0*reserve1)与当前储备金的差值。若有手续费收益,铸造对应 LP 代币给工厂合约。影响:手续费会略微增加 totalSupply,从而影响后续 LP 销毁计算。3. 计算应返还的代币量amount0 = liquidity.mul(balance0) / _totalSupply; amount1 = liquidity.mul(balance1) / _totalSupply; require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');数学公式:关键点:使用 当前余额(balance0) 而非 储备金(_reserve0),确保包含未结算的手续费,实现按比例公平分配。必须返还两种代币(防止只提取单一代币的攻击)。4. 销毁 LP 并转账代币_burn(address(this), liquidity); // 销毁LP _safeTransfer(_token0, to, amount0); // 转代币0给用户 _safeTransfer(_token1, to, amount1); // 转代币1给用户原子性:先销毁 LP,再转账代币,防止重入攻击。安全转账:_safeTransfer 会验证代币合约的返回值,防止恶意代币导致资金锁定。5. 更新储备金与触发事件balance0 = IERC20(_token0).balanceOf(address(this)); // 更新余额 balance1 = IERC20(_token1).balanceOf(address(this)); _update(balance0, balance1, _reserve0, _reserve1); // 同步储备金 if (feeOn) kLast = uint(reserve0).mul(reserve1); // 更新kLast emit Burn(msg.sender, amount0, amount1, to); // 事件日志_update:将最新余额写入储备金,并计算累计价格(用于 TWAP 预言机)。kLast:仅在手续费开启时更新,作为下次手续费计算的基准。分叉主网测试removeLiquiditycontract UniswapV2LiquidityTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_WETH); address private constant user = address(100); function setUp() public { // Fund WETH to user deal(user, 100 * 1e18); vm.startPrank(user); weth.deposit{value: 100 * 1e18}(); weth.approve(address(router), type(uint256).max); vm.stopPrank(); // Fund DAI to user deal(DAI, user, 1000000 * 1e18); vm.startPrank(user); dai.approve(address(router), type(uint256).max); vm.stopPrank(); } function test_removeLiquidity() public { vm.startPrank(user); (,, uint256 liquidity) = router.addLiquidity({ tokenA: DAI, tokenB: WETH, amountADesired: 1000000 * 1e18, amountBDesired: 100 * 1e18, amountAMin: 1, amountBMin: 1, to: user, deadline: block.timestamp }); pair.approve(address(router), liquidity); (uint256 amountA, uint256 amountB) = router.removeLiquidity(WETH, DAI, liquidity, 1, 1, user, block.timestamp); vm.stopPrank(); console2.log("DAI:", amountA); console2.log("WETH:", amountB); assertEq(pair.balanceOf(user), 0, "LP = 0"); } }
2025年08月05日
1 阅读
0 评论
1 点赞
2025-08-02
Uniswap v2-Swap
Uniswap v2一、Swap单个币种之间swap的流程多个币种之间swap的流程1.swapExactTokensForTokens 和 swapTokensForExactTokens这个函数是 Uniswap V2 路由器合约中的一个重要功能,用于将确切数量的某种代币交换为另一种代币(可能经过多个交易对)。// v2-periphery/contracts/UniswapV2Router02.sol // swapExactTokensForTokens (精确输入,不确定输出) // 功能:用确切数量的输入代币换取尽可能多的输出代币 function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) { amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); } // swapTokensForExactTokens 精确输出,不确定输入 // 用尽可能少的输入代币换取确切数量的输出代币 function swapTokensForExactTokens( uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) { amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); }参数解释amountIn: 你想交换的输入代币数量amountOutMin: 你愿意接受的最小输出代币数量(防止高滑点)path: 交换路径(代币地址数组)to: 接收输出代币的地址deadline: 交易有效期限(Unix 时间戳)path 参数path 数组表示从代币 A 到代币 B 可能经过的所有中间代币。例如:直接交换:[代币A, 代币B]通过中间代币交换:[代币A, 代币WETH, 代币B]函数会按照路径顺序一一交换,直到最后一个目标币种。factory 参数factory 确实是指 UniswapV2Factory 合约(核心合约之一),它负责创建和管理交易对。amounts 数组amounts 是通过 UniswapV2Library.getAmountsOut() 计算得到的数组,它包含:沿交换路径每个阶段预期的代币数量数组的第一个元素是 amountIn(输入数量)最后一个元素是预期的输出代币数量中间元素是每个中间交换步骤的预期数量例如,对于路径 [A, B, C],amounts 可能看起来像 [100 A, 50 B, 25 C],表示:用 100 A 换 50 B再用 50 B 换 25 C函数执行流程计算沿路径的预期输出量 (getAmountsOut)检查最终输出是否满足用户的最小要求 (amountOutMin)将输入代币从用户转移到第一个交易对执行沿路径的交换 (_swap 内部函数)返回各阶段的实际交换数量这种设计允许复杂的多跳交换,同时确保用户获得至少他们指定的最小输出量。分叉主网测试getAmountsOut和getAmountsInforge test --fork-url $FORK_URL -vvv// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../../src/interfaces/IERC20.sol"; import {IWETH} from "../../src/interfaces/IWETH.sol"; import {IUniswapV2Router02} from "../../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import {DAI, WETH, MKR, UNISWAP_V2_ROUTER_02} from "../../src/Constants.sol"; contract UniswapV2SwapAmountsTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IERC20 private constant mkr = IERC20(MKR); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); function test_getAmountsOut() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountIn = 1e18; uint256[] memory amounts = router.getAmountsOut(amountIn, path); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); } function test_getAmountsIn() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountOut = 1e16; uint256[] memory amounts = router.getAmountsIn(amountOut, path); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); } } 2._swap最终执行两个token之间转换的核心函数 // v2-periphery/contracts/UniswapV2Router02.sol function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { for (uint i; i < path.length - 1; i++) { (address input, address output) = (path[i], path[i + 1]); (address token0,) = UniswapV2Library.sortTokens(input, output); uint amountOut = amounts[i + 1]; (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( amount0Out, amount1Out, to, new bytes(0) ); } }函数参数amounts:交易路径上每个代币的预期数量数组(由 getAmountsOut 计算得出)path:代币交换路径(如 [代币A, 代币B, 代币C])_to:最终接收代币的地址核心逻辑分步解析步骤1:遍历交易路径for (uint i; i < path.length - 1; i++) { (address input, address output) = (path[i], path[i + 1]);循环处理路径中的每一跳(如 A→B→C 需要处理 A→B 和 B→C 两跳)input 和 output 分别表示当前交易的输入代币和输出代币步骤2:排序代币确定交易对(address token0,) = UniswapV2Library.sortTokens(input, output);Uniswap 交易对中的代币按地址排序存储(token0 < token1)通过排序确定代币在交易对中的顺序(影响 amount0Out 和 amount1Out 的赋值)步骤3:确定输出量uint amountOut = amounts[i + 1]; (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));amountOut 是当前交易对的预期输出量(来自 amounts 数组)关键规则:如果 input 是 token0,则 amount0Out = 0,amount1Out = amountOut(因为输入是 token0,输出是 token1)如果 input 是 token1,则 amount0Out = amountOut,amount1Out = 0(输入是 token1,输出是 token0)步骤4:确定接收地址address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;如果是中间交易(如 A→B→C 中的 A→B):to 是下一个交易对的地址(B/C 的交易对)如果是最后一跳(如 B→C):to 是用户指定的最终地址 _to步骤5:执行交易IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( amount0Out, amount1Out, to, new bytes(0) );通过 pairFor 计算当前交易对的地址调用交易对的 swap 方法,传递输出量和接收地址new bytes(0) 表示无回调数据(非闪电贷场景)_swap调用了IUniswapV2Pair的pairFor.swap函数// v2-core/contracts/UniswapV2Pair.sol // this low-level function should be called from a contract which performs important safety checks function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); } _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); }函数参数amount0Out:从储备中提取的 token0 数量amount1Out:从储备中提取的 token1 数量to:接收输出代币的地址data:回调数据(用于闪电贷等高级操作)主要逻辑步骤初步检查require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');确保至少有一个输出量大于0(不能两个都为0)使用getReserves来获取状态变量 在同一个快照时间获取数据保持数据一致性 同时这三个变量由于在一个存储槽内会被打包成一个sload操作节省gasuint112 private reserve0; // uses single storage slot, accessible via getReserves uint112 private reserve1; // uses single storage slot, accessible via getReserves uint32 private blockTimestampLast;为了安全会提前检查此时需要swap的两个token目前的余量require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');执行转账这块代码中使用了很多花括号 创建一个新的作用域(Scope),它可以用来 临时限制变量的生命周期,从而帮助解决 "Stack Too Deep"(堆栈过深) 的错误。Solidity 的 EVM 使用 栈(Stack) 来存储局部变量,但 栈的最大深度限制是 16 个槽位(即最多同时存储 16 个局部变量)。{ ... } 内部定义的变量只在块内有效,退出块后,这些变量 不再占用栈空间。这样,编译器可以 复用栈槽位,而不是持续占用新的位置。除了 { ... } 作用域隔离,还可以:使用 memory 或 calldata 存储临时数据(减少栈使用)。拆分成多个函数(减少单个函数的变量数量)。使用结构体(struct)或数组 打包变量(但可能增加 Gas 成本)。内联汇编(assembly) 手动管理栈(高级用法)。{ // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); }获取代币地址确保接收地址不是代币合约本身执行输出代币的转账(乐观转账)处理回调(闪电贷)if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);如果有回调数据,通知接收者(用于闪电贷等场景)计算实际输入量balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;获取新的余额计算实际输入量:如果新余额 > (旧储备 - 输出量),差额就是输入量否则输入量为0先转账后检测,swap之前先讲要转换的token转账到币对合约兼容 ERC-20 标准:ERC-20 的 transferFrom 需要单独授权和转账操作,无法在单次调用中同时完成转入和交换。安全性:强制用户先转账可以防止重入攻击(转账和交换分离)。检查恒定乘积公式uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');计算调整后的余额(考虑0.3%手续费)确保 (adjustedBalance0 × adjustedBalance1) ≥ (旧储备0 × 旧储备1)这是Uniswap恒定乘积公式的核心检查更新状态_update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);更新储备量触发Swap事件function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); }(1) 溢出检查require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');作用:确保余额不超过 uint112 的最大值($2^{112}-1$)。uint112(-1) 是 Solidity 中获取该类型最大值的方式(等价于 type(uint112).max)。(2) 时间处理uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired时间截断:block.timestamp % 2**32 确保时间戳在 uint32 范围内(约 136 年循环一次)。时间差计算:timeElapsed 允许溢出(如时间戳回绕时),因为 Uniswap 依赖时间差模运算的正确性。例如:若 blockTimestampLast = 2^32-1,blockTimestamp = 0,则 timeElapsed = 1(符合预期)。(3) 累计价格更新if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; }触发条件:时间已流逝(timeElapsed > 0)。储备金非零(防止除零错误)。价格计算:price0CumulativeLast:记录 token1/token0 的累计价格(单位为秒)。使用 UQ112x112 定点数库保持精度(112 位整数 + 112 位小数)。price1CumulativeLast:同理记录 token0/token1 的价格。为什么需要时间加权?抗操纵性市场价格可能被瞬时操纵(如闪电贷攻击)。TWAP 通过长时间累积平滑价格波动,外部合约可查询两个时间点的累计价差,再除以时间差得到平均价格:TWAP = priceCumulative(t2)−priceCumulative(t1) / (t2−t1)设计目的:支持链下价格预言机(如 TWAP 时间加权平均价格)。累计价格随时间线性增长,外部合约可通过差值计算时间段内的平均价格。(4) 储备金和时间戳更新reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1);更新状态:将实时余额写入储备金(reserve0/reserve1)。记录当前时间戳(blockTimestampLast)。事件:触发 Sync 事件,通知外部监听者储备金变化。分叉主网测试swapExactTokensForTokens和swapTokensForExactTokens// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; import {IWETH} from "../src/interfaces/IWETH.sol"; import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol"; import {DAI, WETH, MKR, UNISWAP_V2_PAIR_DAI_MKR, UNISWAP_V2_ROUTER_02} from "../../../src/Constants.sol"; contract UniswapV2SwapTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IERC20 private constant mkr = IERC20(MKR); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_MKR); address private constant user = address(100); function setUp() public { deal(user, 100 * 1e18); vm.startPrank(user); weth.deposit{value: 100 * 1e18}(); weth.approve(address(router), type(uint256).max); vm.stopPrank(); // Add MKR liquidity to DAI/MKR pool deal(DAI, address(pair), 1e6 * 1e18); deal(MKR, address(pair), 1e5 * 1e18); pair.sync(); } // Swap all input tokens for as many output tokens as possible function test_swapExactTokensForTokens() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountIn = 1e18; uint256 amountOutMin = 1; vm.prank(user); uint256[] memory amounts = router.swapExactTokensForTokens({ amountIn: amountIn, amountOutMin: amountOutMin, path: path, to: user, deadline: block.timestamp }); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); assertGe(mkr.balanceOf(user), amountOutMin, "MKR balance of user"); } // Receive an exact amount of output tokens for as few input tokens // as possible function test_swapTokensForExactTokens() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountOut = 0.1 * 1e18; uint256 amountInMax = 1e18; vm.prank(user); uint256[] memory amounts = router.swapTokensForExactTokens({ amountOut: amountOut, amountInMax: amountInMax, path: path, to: user, deadline: block.timestamp }); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); assertEq(mkr.balanceOf(user), amountOut, "MKR balance of user"); } }
2025年08月02日
1 阅读
0 评论
1 点赞