从入门到实战:我的全套 Web3 学习路径(2025版)
你好,我是 Keegan 小钢。 如果你刚开始接触 Web3、准备从 Web2 转型,或者正在寻找一套真正系统、可落地、能带你从入门做到链上实战的学习路径,那么这篇文章会对你非常有帮助。 我是一名有 16 年经验的互联网从业者,过去 8 年专注于 Web3 技术方向,同时持续做个人 IP 超过 13 年。长期以来,我在公众号、知乎、B 站持续输出 Web3 的学习路线、开发知识、工程实践以及真实链上项目的研发过程。如果你对我的经历好奇,可以阅读这篇文章:《复盘我的 13 年个人 IP 之路》。 这几年,越来越多同学加我咨询,他们大多会问:我应该从哪里开始学 Web3?我有一些基础,但做不出完整项目怎么办?有没有适合“从入门到能做项目”的学习路径?我想顺利找到 Web3 工作,该怎么准备?本质上,这些问题可以归结成一句话:“我现在这个水平,下一步该学什么?”为了让不同阶段的同学都能快速找到最适合自己的学习路径,我把过去几年输出的所有 Web3 内容——免费课程、付费课程、AI+Web3 实战营、以及深度服务——做了一次系统性的梳理。 这篇文章是你最清晰、最完整的 「Web3 学习路...
万字长文聊聊Web3的现状与趋势
整体数据现状与趋势首先,先来看看 Web3 的搜索热度情况,我们可以从 GoogleTrends 中看到一些数据。下图是关于 Web3 在全球过去 5 年内的搜索热度趋势图:从图中可以看出,前面几年的搜索热度一直很低,热度值一直保持在 10 以下,但从 2021 年下半年开始逐渐飙升,在 2021 年 12 月底达到了顶峰。虽然随后开始有所回落,但依然保持在很高的热度。 如果再按区域来看搜索热度,就会发现,搜索热度最高的竟然是在中国,且与其他区域的搜索热度差距很大,如下图所示这说明,中国依然是 Web3 最大的潜在市场。 接着,再来看看整个加密货币总市值的趋势图,某种程度上,这也代表了整个 Web3 行业的总市值。下图的数据来自 CoinMarketCap:从图中可以看到,总市值也是在 2021 年出现大幅度飙升,2021 年底到达顶峰,达到了将近 3 万亿美元的总市值。随后不断回落,在 2022 年底跌到了最低点,总市值降到低于 1 万亿,相比高点,跌去了三分之二。但是,就算是最低点也依然比 2021 年之前那些年的总市值高得多。 加密货币的总市值看上去好像不低,但如果跟全球股...
万字长文聊聊Web3的组成架构
Web3 发展至今,生态已然初具雏形,如果将当前阶段的 Web3 生态组成架构抽象出一个鸟瞰图,由下而上可划分为四个层级:区块链网络层、中间件层、应用层、访问层。下面我们来具体看看每一层级都有什么。另外,此章节会涉及到很多项目的名称,因为篇幅原因不会一一进行介绍,有兴趣的可以另外去查阅相关资料进行深入了解。区块链网络层最底层是「区块链网络层」,也是 Web3 的基石层,主要由各区块链网络所组成。 组成该层级的区块链网络还不少,Bitcoin、Ethereum、BNB Chain(BSC)、Polygon、Arbitrum、Polkadot、Cosmos、Celestia、Avalanche、Aptos、Sui 等等,还有很多。根据 Blockchain-Comparison 的统计,截止撰文之日的区块链至少有 150 条。这里我们主要说的是公链,联盟链不包括在内。因为区块链实在太多,会有些眼花缭乱,所以有必要进行分门别类。 首先,不同区块链之间存在着分层结构,有 Layer0、Layer1、Layer2 之分。其次,Web3 的繁荣发展,依赖于智能合约技术,而智能合约的运行环境为...
Blockchain engineer
从入门到实战:我的全套 Web3 学习路径(2025版)
你好,我是 Keegan 小钢。 如果你刚开始接触 Web3、准备从 Web2 转型,或者正在寻找一套真正系统、可落地、能带你从入门做到链上实战的学习路径,那么这篇文章会对你非常有帮助。 我是一名有 16 年经验的互联网从业者,过去 8 年专注于 Web3 技术方向,同时持续做个人 IP 超过 13 年。长期以来,我在公众号、知乎、B 站持续输出 Web3 的学习路线、开发知识、工程实践以及真实链上项目的研发过程。如果你对我的经历好奇,可以阅读这篇文章:《复盘我的 13 年个人 IP 之路》。 这几年,越来越多同学加我咨询,他们大多会问:我应该从哪里开始学 Web3?我有一些基础,但做不出完整项目怎么办?有没有适合“从入门到能做项目”的学习路径?我想顺利找到 Web3 工作,该怎么准备?本质上,这些问题可以归结成一句话:“我现在这个水平,下一步该学什么?”为了让不同阶段的同学都能快速找到最适合自己的学习路径,我把过去几年输出的所有 Web3 内容——免费课程、付费课程、AI+Web3 实战营、以及深度服务——做了一次系统性的梳理。 这篇文章是你最清晰、最完整的 「Web3 学习路...
万字长文聊聊Web3的现状与趋势
整体数据现状与趋势首先,先来看看 Web3 的搜索热度情况,我们可以从 GoogleTrends 中看到一些数据。下图是关于 Web3 在全球过去 5 年内的搜索热度趋势图:从图中可以看出,前面几年的搜索热度一直很低,热度值一直保持在 10 以下,但从 2021 年下半年开始逐渐飙升,在 2021 年 12 月底达到了顶峰。虽然随后开始有所回落,但依然保持在很高的热度。 如果再按区域来看搜索热度,就会发现,搜索热度最高的竟然是在中国,且与其他区域的搜索热度差距很大,如下图所示这说明,中国依然是 Web3 最大的潜在市场。 接着,再来看看整个加密货币总市值的趋势图,某种程度上,这也代表了整个 Web3 行业的总市值。下图的数据来自 CoinMarketCap:从图中可以看到,总市值也是在 2021 年出现大幅度飙升,2021 年底到达顶峰,达到了将近 3 万亿美元的总市值。随后不断回落,在 2022 年底跌到了最低点,总市值降到低于 1 万亿,相比高点,跌去了三分之二。但是,就算是最低点也依然比 2021 年之前那些年的总市值高得多。 加密货币的总市值看上去好像不低,但如果跟全球股...
万字长文聊聊Web3的组成架构
Web3 发展至今,生态已然初具雏形,如果将当前阶段的 Web3 生态组成架构抽象出一个鸟瞰图,由下而上可划分为四个层级:区块链网络层、中间件层、应用层、访问层。下面我们来具体看看每一层级都有什么。另外,此章节会涉及到很多项目的名称,因为篇幅原因不会一一进行介绍,有兴趣的可以另外去查阅相关资料进行深入了解。区块链网络层最底层是「区块链网络层」,也是 Web3 的基石层,主要由各区块链网络所组成。 组成该层级的区块链网络还不少,Bitcoin、Ethereum、BNB Chain(BSC)、Polygon、Arbitrum、Polkadot、Cosmos、Celestia、Avalanche、Aptos、Sui 等等,还有很多。根据 Blockchain-Comparison 的统计,截止撰文之日的区块链至少有 150 条。这里我们主要说的是公链,联盟链不包括在内。因为区块链实在太多,会有些眼花缭乱,所以有必要进行分门别类。 首先,不同区块链之间存在着分层结构,有 Layer0、Layer1、Layer2 之分。其次,Web3 的繁荣发展,依赖于智能合约技术,而智能合约的运行环境为...
Blockchain engineer

Subscribe to Keegan小钢

Subscribe to Keegan小钢
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
SwapRouter 合约封装了面向用户的交易接口,但不再像 UniswapV2Router 一样根据不同交易场景拆分为了那么多函数,UniswapV3 的 SwapRouter 核心就只有 4 个交易函数:
exactInputSingle:指定输入数量的单池内交易
exactOutputSingle:指定输出数量的单池内交易
exactInput:指定输入数量和交易路径的交易
exactOutput:指定输出数量和交易路径的交易
带 Single 的只支持单池内的交易,而不带 Single 的则支持跨不同池子的互换交易。
先来看简单的单池交易,以 exactInputSingle 为始,其代码实现如下:
struct ExactInputSingleParams {
address tokenIn; //输入token
address tokenOut; //输出token
uint24 fee; //手续费率
address recipient; //收款地址
uint256 deadline; //过期时间
uint256 amountIn; //指定的输入token数量
uint256 amountOutMinimum; //输出token的最小数量
uint160 sqrtPriceLimitX96; //限定的价格
}
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountOut)
{
amountOut = exactInputInternal(
params.amountIn,
params.recipient,
params.sqrtPriceLimitX96,
SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender})
);
require(amountOut >= params.amountOutMinimum, 'Too little received');
}
其入参有 9 个参数,返回值就一个 amountOut,即输出的 token 数量。
从代码上可看出,实际的逻辑实现是在内部函数 exactInputInternal。查看该内部函数之前,我们先来了解下 SwapCallbackData。我们从上面代码可以看到,调用 exactInputInternal 时,最后一个传入的参数就是 SwapCallbackData,这其实是一个结构体,定义了两个属性:
struct SwapCallbackData {
bytes path;
address payer;
}
path 表示交易路径,在以上代码中,就是由 tokenIn、fee、tokenOut 这三个变量拼接而成。payer 表示支付输入 token 的地址,上面的就是 msg.sender。
接着,来看看内部函数 exactInputInternal 的代码实现:
function exactInputInternal(
uint256 amountIn,
address recipient,
uint160 sqrtPriceLimitX96,
SwapCallbackData memory data
) private returns (uint256 amountOut) {
// allow swapping to the router address with address 0
if (recipient == address(0)) recipient = address(this);
//从路径中解码出第一个池子
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
//当tokenIn<tokenOUt时,则说明tokenIn为token0,所以是要将token0兑换成token1
bool zeroForOne = tokenIn < tokenOut;
//调用底层池子的swap函数执行交易
(int256 amount0, int256 amount1) =
getPool(tokenIn, tokenOut, fee).swap(
recipient,
zeroForOne,
amountIn.toInt256(),
sqrtPriceLimitX96 == 0
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
: sqrtPriceLimitX96,
abi.encode(data)
);
//返回amountOut
return uint256(-(zeroForOne ? amount1 : amount0));
}
首先,如果 recipient 地址为零地址的话,那会把 recipient 重置为当前合约地址。
接着,通过 data.path.decodeFirstPool() 从路径中解码得出 tokenIn、tokenOut 和 fee。decodeFirstPool 函数是在库合约 Path 里实现的。
布尔类型的 zeroForOne 表示底层 token0 和 token1 的兑换方向,为 true 表示用 token0 兑换 token1,false 则反之。因为底层的 token0 是小于 token1 的,所以,当 tokenIn 也小于 tokenOut 的时候,说明 tokenIn == token0,所以 zeroForOne 为 true。
然后,通过 getPool 函数可得到池子地址,再调用底层池子的 swap 函数来执行实际的交易逻辑。
最后,我们要得到的是 amountOut,这是 amount0 和 amount1 中的其中一个。我们已经知道,zeroForOne 为 true 的时候,tokenIn 等于 token0,所以 tokenOut 就是 token1,因此 amountOut 就是 amount1。另外,对底层池子来说,属于输出的时候,返回的数值是负数,即 amount1 其实是一个负数,因此需要再加个负号转为正数的 uint256 类型。
在这个函数里,我们可以看出并没有支付 token 的功能,但前面讲解 UniswapV3Pool 时已经了解到,支付是在回调函数 uniswapV3SwapCallback 里完成的。因为这个回调函数会涉及到所有 4 种交易类型,所以我们留到最后再来讲解。
接着,来看 exactOutputSingle 函数的实现,其代码如下:
struct ExactOutputSingleParams {
address tokenIn; //输入token
address tokenOut; //输出token
uint24 fee; //手续费率
address recipient; //收款地址
uint256 deadline; //过期时间
uint256 amountOut; //指定的输出token数量
uint256 amountInMaximum; //输入token的最大数量
uint160 sqrtPriceLimitX96; //限定的价格
}
function exactOutputSingle(ExactOutputSingleParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountIn)
{
// avoid an SLOAD by using the swap return data
amountIn = exactOutputInternal(
params.amountOut,
params.recipient,
params.sqrtPriceLimitX96,
SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender})
);
require(amountIn <= params.amountInMaximum, 'Too much requested');
// has to be reset even though we don't use it in the single hop case
amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}
可看出,exactOutputSingle 函数的实现与 exactInputSingle 函数大同小异。首先,参数上,只有两个不同,exactInputSingle 函数指定的是 amountIn 和 amountOutMinimum;而 exactOutputSingle 函数改为了 amountOut 和 amountInMaximum,即输出是指定的,而输入则限制了最大值。其次,实际逻辑封装在了 exactOutputInternal 内部函数,而且传给该内部函数的最后一个参数的 path 组装顺序也不一样了,排在第一位的是 tokenOut。
核心实现还是在 exactOutputInternal 内部函数,其代码实现如下:
function exactOutputInternal(
uint256 amountOut,
address recipient,
uint160 sqrtPriceLimitX96,
SwapCallbackData memory data
) private returns (uint256 amountIn) {
// allow swapping to the router address with address 0
if (recipient == address(0)) recipient = address(this);
//从路径中解码出第一个池子
(address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();
//是否token0兑换token1
bool zeroForOne = tokenIn < tokenOut;
//调用底层池子的swap函数执行交易
(int256 amount0Delta, int256 amount1Delta) =
getPool(tokenIn, tokenOut, fee).swap(
recipient,
zeroForOne,
-amountOut.toInt256(), //指定输出需转为负数
sqrtPriceLimitX96 == 0
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
: sqrtPriceLimitX96,
abi.encode(data)
);
//确定amountIn和amountOut
uint256 amountOutReceived;
(amountIn, amountOutReceived) = zeroForOne
? (uint256(amount0Delta), uint256(-amount1Delta))
: (uint256(amount1Delta), uint256(-amount0Delta));
// it's technically possible to not receive the full output amount,
// so if no price limit has been specified, require this possibility away
if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);
}
可见和 exactInputInternal 的实现也是大同小异。不过,有一个细节需要补充一下。因为是指定的输出数额,所以调用底层的 swap 函数时,第三个传参转为了负数,这也是前面讲解 UniswapV3Pool 的 swap 函数时讲过的,当指定的交易数额是输出的数额时,则需传负数。
和 exactInputInternal 一样,在当前函数里没有支付 token 的逻辑,也是统一在 uniswapV3SwapCallback 回调函数里去完成支付。
exactInput 函数则用于处理跨多个池子的指定输入数量的交易,相比单池交易会复杂一些,而且这里面的逻辑还有点绕,我们来进行一一剖析。其实现代码如下:
struct ExactInputParams {
bytes path; //交易路径
address recipient; //收款地址
uint256 deadline; //过期时间
uint256 amountIn; //指定输入token数量
uint256 amountOutMinimum; //输出token的最小数量
}
function exactInput(ExactInputParams memory params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountOut)
{
//调用者需支付路径中的第一个代币
address payer = msg.sender;
//遍历路径
while (true) {
//路径中是否还存在多个池子
bool hasMultiplePools = params.path.hasMultiplePools();
//先前交换的输出成为后续交换的输入
params.amountIn = exactInputInternal(
params.amountIn,
hasMultiplePools ? address(this) : params.recipient,
0,
SwapCallbackData({
path: params.path.getFirstPool(), // 只需要路径里的第一个池子
payer: payer
})
);
//当路径依然由多个池子组成时,则继续循环,否则退出循环
if (hasMultiplePools) {
payer = address(this);
//跳过第一个token,作为下一轮的路径
params.path = params.path.skipToken();
} else {
//最后一次兑换,把前面设为了amountIn的重新赋值给amountOut
amountOut = params.amountIn;
break;
}
}
require(amountOut >= params.amountOutMinimum, 'Too little received');
}
其中,需要跨多个池子的路径编码方式如下图:

和 UniswapV2 一样,这个路径是由前端计算出来再传给合约的。寻找最优路径的算法也是和 UniswapV2 一样的思路。
exactInput 函数的核心实现逻辑是,循环处理路径中的每一个配对池,每处理完一个池子的交易,就从路径中移除第一个 token 和 fee,直到路径只剩下最后一个池子就结束循环。期间,每一次执行 exactInputInternal 后,将返回的 amounOut 作为下一轮的 amountIn。第一轮兑换时,payer 是合约的调用者,即 msg.sender,而输出代币的 recipient 则是当前合约地址。中间的每一次兑换,payer 和 recipient 都是当前合约地址。到最后一次兑换时,recipient 才转为用户传入的地址。
剩下最后一个函数 exactOutput 了,也是用于处理跨多个池子的的交易,而指定的是输出的数量。以下是其代码实现:
struct ExactOutputParams {
bytes path; //交易路径
address recipient; //收款地址
uint256 deadline; //过期时间
uint256 amountOut; //指定输出token数量
uint256 amountInMaximum; //输入token的最大数量
}
function exactOutput(ExactOutputParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountIn)
{
// it's okay that the payer is fixed to msg.sender here, as they're only paying for the "final" exact output
// swap, which happens first, and subsequent swaps are paid for within nested callback frames
exactOutputInternal(
params.amountOut,
params.recipient,
0,
SwapCallbackData({path: params.path, payer: msg.sender})
);
amountIn = amountInCached;
require(amountIn <= params.amountInMaximum, 'Too much requested');
amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}
可看到其逻辑就直接调用内部函数 exactOutputInternal 完成交易,并没有像 exactInput 一样的循环处理。但在整个流程中,其实还是进行了遍历路径的多次交易的,只是这个流程完成得比较隐晦。其关键其实是在 uniswapV3SwapCallback 回调函数里,后面我们会说到。
以下就是回调函数的实现:
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata _data
) external override {
require(amount0Delta > 0 || amount1Delta > 0);
//解码出_data数据
SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
//解码出路径的第一个池子
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
//校验callback的调用者
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
//用于判断当前需要支付的代币
(bool isExactInput, uint256 amountToPay) =
amount0Delta > 0
? (tokenIn < tokenOut, uint256(amount0Delta))
: (tokenOut < tokenIn, uint256(amount1Delta));
if (isExactInput) { //指定金额的是输入,直接执行支付
pay(tokenIn, data.payer, msg.sender, amountToPay);
} else { //指定金额的是输出
// either initiate the next swap or pay
if (data.path.hasMultiplePools()) {
// 路径里有多个池子时,则跳过路径的第一个token,使用下一个配对的池子进行交易
data.path = data.path.skipToken();
exactOutputInternal(amountToPay, msg.sender, 0, data);
} else { //只剩下一个池子,执行支付
amountInCached = amountToPay;
tokenIn = tokenOut; // swap in/out because exact output swaps are reversed
pay(tokenIn, data.payer, msg.sender, amountToPay);
}
}
}
另外,这个是 swap 时的回调函数。而之前的文章我们还讲了另一个回调函数 uniswapV3MintCallback 是添加流动性时的回调函数,两者是不同的,不要搞混了。
其逻辑实现并不复杂。首先,先把 _data 解码成 SwapCallbackData 结构体类型数据。接着,解码出路径的第一个池子。然后,通过 verifyCallback 校验调用当前回调函数的是否为底层 pool 合约,非底层 pool 合约是不允许调起回调函数的。
isExactInput 和 amountToPay 的赋值需要拆解一下才好理解。首先需知道,amount0Delta 和 amount1Delta 其实是一正一负的,正数是输入的,负数是输出的。因此,amount0Delta 大于 0 的话则 amountToPay 就是 amount0Delta,否则就是 amount1Delta 了。 amount0Delta 大于 0 也说明了输入的是 token0,因此,当 tokenIn < tokenOut 的时候,说明 tokenIn 就是 token0,也即是说用户指定的是输入数量,所以这时候的 isExactInput 即为 true。
当指定金额为输出的时候,也就是处理 exactOutput 和 exactOutputSingle 函数的时候。我们前面看到 exactOutput 的代码逻辑里并没有对路径进行遍历处理,这个遍历其实就是在这个回调函数里完成的。仔细看这段代码:
if (data.path.hasMultiplePools()) {
// 路径里有多个池子时,则跳过路径的第一个token,使用下一个配对的池子进行交易
data.path = data.path.skipToken();
exactOutputInternal(amountToPay, msg.sender, 0, data);
}
这不就是遍历路径多次执行 exactOutputInternal 了吗。
至此,SwapRouter 合约也讲解完了。
SwapRouter 合约封装了面向用户的交易接口,但不再像 UniswapV2Router 一样根据不同交易场景拆分为了那么多函数,UniswapV3 的 SwapRouter 核心就只有 4 个交易函数:
exactInputSingle:指定输入数量的单池内交易
exactOutputSingle:指定输出数量的单池内交易
exactInput:指定输入数量和交易路径的交易
exactOutput:指定输出数量和交易路径的交易
带 Single 的只支持单池内的交易,而不带 Single 的则支持跨不同池子的互换交易。
先来看简单的单池交易,以 exactInputSingle 为始,其代码实现如下:
struct ExactInputSingleParams {
address tokenIn; //输入token
address tokenOut; //输出token
uint24 fee; //手续费率
address recipient; //收款地址
uint256 deadline; //过期时间
uint256 amountIn; //指定的输入token数量
uint256 amountOutMinimum; //输出token的最小数量
uint160 sqrtPriceLimitX96; //限定的价格
}
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountOut)
{
amountOut = exactInputInternal(
params.amountIn,
params.recipient,
params.sqrtPriceLimitX96,
SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender})
);
require(amountOut >= params.amountOutMinimum, 'Too little received');
}
其入参有 9 个参数,返回值就一个 amountOut,即输出的 token 数量。
从代码上可看出,实际的逻辑实现是在内部函数 exactInputInternal。查看该内部函数之前,我们先来了解下 SwapCallbackData。我们从上面代码可以看到,调用 exactInputInternal 时,最后一个传入的参数就是 SwapCallbackData,这其实是一个结构体,定义了两个属性:
struct SwapCallbackData {
bytes path;
address payer;
}
path 表示交易路径,在以上代码中,就是由 tokenIn、fee、tokenOut 这三个变量拼接而成。payer 表示支付输入 token 的地址,上面的就是 msg.sender。
接着,来看看内部函数 exactInputInternal 的代码实现:
function exactInputInternal(
uint256 amountIn,
address recipient,
uint160 sqrtPriceLimitX96,
SwapCallbackData memory data
) private returns (uint256 amountOut) {
// allow swapping to the router address with address 0
if (recipient == address(0)) recipient = address(this);
//从路径中解码出第一个池子
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
//当tokenIn<tokenOUt时,则说明tokenIn为token0,所以是要将token0兑换成token1
bool zeroForOne = tokenIn < tokenOut;
//调用底层池子的swap函数执行交易
(int256 amount0, int256 amount1) =
getPool(tokenIn, tokenOut, fee).swap(
recipient,
zeroForOne,
amountIn.toInt256(),
sqrtPriceLimitX96 == 0
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
: sqrtPriceLimitX96,
abi.encode(data)
);
//返回amountOut
return uint256(-(zeroForOne ? amount1 : amount0));
}
首先,如果 recipient 地址为零地址的话,那会把 recipient 重置为当前合约地址。
接着,通过 data.path.decodeFirstPool() 从路径中解码得出 tokenIn、tokenOut 和 fee。decodeFirstPool 函数是在库合约 Path 里实现的。
布尔类型的 zeroForOne 表示底层 token0 和 token1 的兑换方向,为 true 表示用 token0 兑换 token1,false 则反之。因为底层的 token0 是小于 token1 的,所以,当 tokenIn 也小于 tokenOut 的时候,说明 tokenIn == token0,所以 zeroForOne 为 true。
然后,通过 getPool 函数可得到池子地址,再调用底层池子的 swap 函数来执行实际的交易逻辑。
最后,我们要得到的是 amountOut,这是 amount0 和 amount1 中的其中一个。我们已经知道,zeroForOne 为 true 的时候,tokenIn 等于 token0,所以 tokenOut 就是 token1,因此 amountOut 就是 amount1。另外,对底层池子来说,属于输出的时候,返回的数值是负数,即 amount1 其实是一个负数,因此需要再加个负号转为正数的 uint256 类型。
在这个函数里,我们可以看出并没有支付 token 的功能,但前面讲解 UniswapV3Pool 时已经了解到,支付是在回调函数 uniswapV3SwapCallback 里完成的。因为这个回调函数会涉及到所有 4 种交易类型,所以我们留到最后再来讲解。
接着,来看 exactOutputSingle 函数的实现,其代码如下:
struct ExactOutputSingleParams {
address tokenIn; //输入token
address tokenOut; //输出token
uint24 fee; //手续费率
address recipient; //收款地址
uint256 deadline; //过期时间
uint256 amountOut; //指定的输出token数量
uint256 amountInMaximum; //输入token的最大数量
uint160 sqrtPriceLimitX96; //限定的价格
}
function exactOutputSingle(ExactOutputSingleParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountIn)
{
// avoid an SLOAD by using the swap return data
amountIn = exactOutputInternal(
params.amountOut,
params.recipient,
params.sqrtPriceLimitX96,
SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender})
);
require(amountIn <= params.amountInMaximum, 'Too much requested');
// has to be reset even though we don't use it in the single hop case
amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}
可看出,exactOutputSingle 函数的实现与 exactInputSingle 函数大同小异。首先,参数上,只有两个不同,exactInputSingle 函数指定的是 amountIn 和 amountOutMinimum;而 exactOutputSingle 函数改为了 amountOut 和 amountInMaximum,即输出是指定的,而输入则限制了最大值。其次,实际逻辑封装在了 exactOutputInternal 内部函数,而且传给该内部函数的最后一个参数的 path 组装顺序也不一样了,排在第一位的是 tokenOut。
核心实现还是在 exactOutputInternal 内部函数,其代码实现如下:
function exactOutputInternal(
uint256 amountOut,
address recipient,
uint160 sqrtPriceLimitX96,
SwapCallbackData memory data
) private returns (uint256 amountIn) {
// allow swapping to the router address with address 0
if (recipient == address(0)) recipient = address(this);
//从路径中解码出第一个池子
(address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();
//是否token0兑换token1
bool zeroForOne = tokenIn < tokenOut;
//调用底层池子的swap函数执行交易
(int256 amount0Delta, int256 amount1Delta) =
getPool(tokenIn, tokenOut, fee).swap(
recipient,
zeroForOne,
-amountOut.toInt256(), //指定输出需转为负数
sqrtPriceLimitX96 == 0
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
: sqrtPriceLimitX96,
abi.encode(data)
);
//确定amountIn和amountOut
uint256 amountOutReceived;
(amountIn, amountOutReceived) = zeroForOne
? (uint256(amount0Delta), uint256(-amount1Delta))
: (uint256(amount1Delta), uint256(-amount0Delta));
// it's technically possible to not receive the full output amount,
// so if no price limit has been specified, require this possibility away
if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);
}
可见和 exactInputInternal 的实现也是大同小异。不过,有一个细节需要补充一下。因为是指定的输出数额,所以调用底层的 swap 函数时,第三个传参转为了负数,这也是前面讲解 UniswapV3Pool 的 swap 函数时讲过的,当指定的交易数额是输出的数额时,则需传负数。
和 exactInputInternal 一样,在当前函数里没有支付 token 的逻辑,也是统一在 uniswapV3SwapCallback 回调函数里去完成支付。
exactInput 函数则用于处理跨多个池子的指定输入数量的交易,相比单池交易会复杂一些,而且这里面的逻辑还有点绕,我们来进行一一剖析。其实现代码如下:
struct ExactInputParams {
bytes path; //交易路径
address recipient; //收款地址
uint256 deadline; //过期时间
uint256 amountIn; //指定输入token数量
uint256 amountOutMinimum; //输出token的最小数量
}
function exactInput(ExactInputParams memory params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountOut)
{
//调用者需支付路径中的第一个代币
address payer = msg.sender;
//遍历路径
while (true) {
//路径中是否还存在多个池子
bool hasMultiplePools = params.path.hasMultiplePools();
//先前交换的输出成为后续交换的输入
params.amountIn = exactInputInternal(
params.amountIn,
hasMultiplePools ? address(this) : params.recipient,
0,
SwapCallbackData({
path: params.path.getFirstPool(), // 只需要路径里的第一个池子
payer: payer
})
);
//当路径依然由多个池子组成时,则继续循环,否则退出循环
if (hasMultiplePools) {
payer = address(this);
//跳过第一个token,作为下一轮的路径
params.path = params.path.skipToken();
} else {
//最后一次兑换,把前面设为了amountIn的重新赋值给amountOut
amountOut = params.amountIn;
break;
}
}
require(amountOut >= params.amountOutMinimum, 'Too little received');
}
其中,需要跨多个池子的路径编码方式如下图:

和 UniswapV2 一样,这个路径是由前端计算出来再传给合约的。寻找最优路径的算法也是和 UniswapV2 一样的思路。
exactInput 函数的核心实现逻辑是,循环处理路径中的每一个配对池,每处理完一个池子的交易,就从路径中移除第一个 token 和 fee,直到路径只剩下最后一个池子就结束循环。期间,每一次执行 exactInputInternal 后,将返回的 amounOut 作为下一轮的 amountIn。第一轮兑换时,payer 是合约的调用者,即 msg.sender,而输出代币的 recipient 则是当前合约地址。中间的每一次兑换,payer 和 recipient 都是当前合约地址。到最后一次兑换时,recipient 才转为用户传入的地址。
剩下最后一个函数 exactOutput 了,也是用于处理跨多个池子的的交易,而指定的是输出的数量。以下是其代码实现:
struct ExactOutputParams {
bytes path; //交易路径
address recipient; //收款地址
uint256 deadline; //过期时间
uint256 amountOut; //指定输出token数量
uint256 amountInMaximum; //输入token的最大数量
}
function exactOutput(ExactOutputParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountIn)
{
// it's okay that the payer is fixed to msg.sender here, as they're only paying for the "final" exact output
// swap, which happens first, and subsequent swaps are paid for within nested callback frames
exactOutputInternal(
params.amountOut,
params.recipient,
0,
SwapCallbackData({path: params.path, payer: msg.sender})
);
amountIn = amountInCached;
require(amountIn <= params.amountInMaximum, 'Too much requested');
amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}
可看到其逻辑就直接调用内部函数 exactOutputInternal 完成交易,并没有像 exactInput 一样的循环处理。但在整个流程中,其实还是进行了遍历路径的多次交易的,只是这个流程完成得比较隐晦。其关键其实是在 uniswapV3SwapCallback 回调函数里,后面我们会说到。
以下就是回调函数的实现:
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata _data
) external override {
require(amount0Delta > 0 || amount1Delta > 0);
//解码出_data数据
SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
//解码出路径的第一个池子
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
//校验callback的调用者
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
//用于判断当前需要支付的代币
(bool isExactInput, uint256 amountToPay) =
amount0Delta > 0
? (tokenIn < tokenOut, uint256(amount0Delta))
: (tokenOut < tokenIn, uint256(amount1Delta));
if (isExactInput) { //指定金额的是输入,直接执行支付
pay(tokenIn, data.payer, msg.sender, amountToPay);
} else { //指定金额的是输出
// either initiate the next swap or pay
if (data.path.hasMultiplePools()) {
// 路径里有多个池子时,则跳过路径的第一个token,使用下一个配对的池子进行交易
data.path = data.path.skipToken();
exactOutputInternal(amountToPay, msg.sender, 0, data);
} else { //只剩下一个池子,执行支付
amountInCached = amountToPay;
tokenIn = tokenOut; // swap in/out because exact output swaps are reversed
pay(tokenIn, data.payer, msg.sender, amountToPay);
}
}
}
另外,这个是 swap 时的回调函数。而之前的文章我们还讲了另一个回调函数 uniswapV3MintCallback 是添加流动性时的回调函数,两者是不同的,不要搞混了。
其逻辑实现并不复杂。首先,先把 _data 解码成 SwapCallbackData 结构体类型数据。接着,解码出路径的第一个池子。然后,通过 verifyCallback 校验调用当前回调函数的是否为底层 pool 合约,非底层 pool 合约是不允许调起回调函数的。
isExactInput 和 amountToPay 的赋值需要拆解一下才好理解。首先需知道,amount0Delta 和 amount1Delta 其实是一正一负的,正数是输入的,负数是输出的。因此,amount0Delta 大于 0 的话则 amountToPay 就是 amount0Delta,否则就是 amount1Delta 了。 amount0Delta 大于 0 也说明了输入的是 token0,因此,当 tokenIn < tokenOut 的时候,说明 tokenIn 就是 token0,也即是说用户指定的是输入数量,所以这时候的 isExactInput 即为 true。
当指定金额为输出的时候,也就是处理 exactOutput 和 exactOutputSingle 函数的时候。我们前面看到 exactOutput 的代码逻辑里并没有对路径进行遍历处理,这个遍历其实就是在这个回调函数里完成的。仔细看这段代码:
if (data.path.hasMultiplePools()) {
// 路径里有多个池子时,则跳过路径的第一个token,使用下一个配对的池子进行交易
data.path = data.path.skipToken();
exactOutputInternal(amountToPay, msg.sender, 0, data);
}
这不就是遍历路径多次执行 exactOutputInternal 了吗。
至此,SwapRouter 合约也讲解完了。
No activity yet