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和getAmountsIn
forge 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操作节省gas
uint112 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需要单独授权和转账操作,无法在单次调用中同时完成转入和交换。 - 安全性:
强制用户先转账可以防止重入攻击(转账和交换分离)。
- 兼容 ERC-20 标准:
检查恒定乘积公式
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");
}
}