在区块链的世界里,DEX的效率,说白了,直接决定了你交易时能不能“多省点、多赚点”。你有没有遇到过这种情况:在Uniswap V3上把 ETH换成USDC,有时候直接兑换就挺划算的,有时候却发现绕个弯,比如先换成WBTC,再换成USDC,反而能多拿一点?
这可不是巧合,而是一套精心设计的最优路径查找算法在起作用。
写这篇文章,一方面是我想整理一下在项目开发中踩过的坑,因为面试中也经常被问到;另一方面,也想带你一起拆解这个“路径优化”的逻辑。我们先从Uniswap V3的询价机制说起,再聊聊路径查找的算法思路,最后附上实战代码,尽量通俗易懂。
先打个生活中的比方:
假设你想把手里的RMB换成美元,银行告诉你有三种方案:
在Uniswap V3中,道理是一样的。不同的交易路径,就像不同的换汇方案,最终的收益可能大相径庭。
现在回到Uniswap中,假设你想把ETH换成USDC,有以下可能的路径:
虽然直觉上直接交换似乎更简单,但实际上不同路径可能产生不同的交易结果,这取决于:
通过寻找最优路径,一笔交易可能节省2%-5%甚至更多的成本,对交易金额大的用户来说尤为重要。
Uniswap V3提供了专门的询价合约:
这些合约的核心功能是 "模拟交易":它们会计算交易的预期结果,但不实际执行交易,非常适合路径查询。
Uniswap V3的询价机制用了一个有趣的技巧 —— 通过 revert 返回数据:
// v3-periphery/contracts/lens/QuoterV2.sol
// 询价合约中的回调函数
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes memory path
) external view override {
// ......
// 计算交换结果
(bool isExactInput, uint256 amountToPay, uint256 amountReceived) =
amount0Delta > 0
? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta))
: (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta));
// 获取交换后的价格状态
IUniswapV3Pool pool = getPool(tokenIn, tokenOut, fee);
(uint160 sqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0();
// 通过revert返回计算结果
if (isExactInput) {
assembly {
let ptr := mload(0x40)
mstore(ptr, amountReceived) // 输出数量
mstore(add(ptr, 0x20), sqrtPriceX96After) // 交换后价格
mstore(add(ptr, 0x40), tickAfter) // 交换后tick
revert(ptr, 96) // 故意revert,但携带96字节的有用数据
}
} else {
// 精确输出的情况...
assembly {
let ptr := mload(0x40)
mstore(ptr, amountToPay)
mstore(add(ptr, 0x20), sqrtPriceX96After)
mstore(add(ptr, 0x40), tickAfter)
revert(ptr, 96) // 故意revert返回数据
}
}
}
// 主要询价函数
function quoteExactInputSingle(QuoteExactInputSingleParams memory params)
public
override
returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)
{
uint256 gasBefore = gasleft();
try
pool.swap(
address(this), // recipient
params.zeroForOne,
params.amountIn.toInt256(),
params.sqrtPriceLimitX96 == 0 ? getDefaultSqrtPriceLimitX96(params.zeroForOne) : params.sqrtPriceLimitX96,
abi.encodePacked(params.tokenIn, params.fee, params.tokenOut)
)
{} catch (bytes memory reason) { // 捕获 故意的错误
gasEstimate = gasBefore - gasleft();
return handleRevert(reason, pool, gasEstimate); // 从错误中提取数据
}
}
/// @dev 解析应包含询价结果的回滚数据
function parseRevertReason(bytes memory reason)
private
pure
returns (
uint256 amount, // 从错误中解析出金额
uint160 sqrtPriceX96After, // 从错误中解析出价格
int24 tickAfter // 从错误中解析出tick
)
{
if (reason.length != 96) { // 检查是否是我们故意构造的96字节错误
if (reason.length < 68) revert('Unexpected error');
assembly {
reason := add(reason, 0x04)
}
revert(abi.decode(reason, (string))); // 处理真正的错误
}
return abi.decode(reason, (uint256, uint160, int24)); // 解码我们的数据
}
工作流程:
具体到方法就是:
QuoterV2.quoteExactInputSingle() → pool.swap() → QuoterV2.uniswapV3SwapCallback() → assembly{revert(ptr, 96)} → try-catch 捕获异常 → parseRevertReason() 解析数据
这种设计的好处是:
了解了Uniswap V3的询价机制后,我们来看看如何实际构建一个路径查找系统。
要找到最优交易路径,需要实现以下关键步骤:
首先建立一个表示代币间交易关系的图结构:
// 池子结构
type Pool struct {
Token0 common.Address // 第一个代币
Token1 common.Address // 第二个代币
Fee int64 // 交易费率(5 = 0.05%, 30 = 0.3%, 100 = 1%)
// 其他池子信息...
}
// 构建交易图
func buildGraph(pools []Pool) map[string]map[string][]Pool {
graph := make(map[string]map[string][]Pool)
for _, pool := range pools {
// 将池子添加为双向边
addToGraph(graph, pool.Token0.Hex(), pool.Token1.Hex(), pool)
addToGraph(graph, pool.Token1.Hex(), pool.Token0.Hex(), pool)
}
return graph
}
func addToGraph(graph map[string]map[string][]Pool, from, to string, pool Pool) {
if _, exists := graph[from]; !exists {
graph[from] = make(map[string][]Pool)
}
graph[from][to] = append(graph[from][to], pool)
}
// 使用DFS查找所有可能路径
func findAllPaths(graph map[string]map[string][]Pool,
start, end common.Address,
maxHops int) []Path {
var paths []Path
visited := make(map[string]bool)
currentPath := Path{
Tokens: []common.Address{start},
Pools: []Pool{},
Fees: []int64{},
}
// 深度优先搜索
var dfs func(current common.Address, depth int)
dfs = func(current common.Address, depth int) {
// 达到目标代币
if current == end && depth > 0 {
pathCopy := copyPath(currentPath)
paths = append(paths, pathCopy)
return
}
// 超出最大跳数
if depth >= maxHops {
return
}
// 标记当前节点为已访问
visited[current.Hex()] = true
// 遍历所有相邻节点
for neighbor, pools := range graph[current.Hex()] {
neighborAddr := common.HexToAddress(neighbor)
if !visited[neighbor] {
for _, pool := range pools {
// 将邻居添加到路径
currentPath.Tokens = append(currentPath.Tokens, neighborAddr)
currentPath.Pools = append(currentPath.Pools, pool)
currentPath.Fees = append(currentPath.Fees, pool.Fee)
// 递归搜索
dfs(neighborAddr, depth+1)
// 回溯 - 从当前路径中移除刚添加的节点,以便尝试其他可能路径
currentPath.Tokens = currentPath.Tokens[:len(currentPath.Tokens)-1]
currentPath.Pools = currentPath.Pools[:len(currentPath.Pools)-1]
currentPath.Fees = currentPath.Fees[:len(currentPath.Fees)-1]
}
}
}
// 取消标记
visited[current.Hex()] = false
}
dfs(start, 0)
return paths
}
Uniswap V3要求将交易路径编码为特定格式:按照token0(20字节) + fee(3字节) + token1(20字节) + fee(3字节) + token2(20字节)...的格式编码,最后一个代币后不添加费率:
// 路径结构
type Path struct {
Pools []Pool
Tokens []common.Address
Fees []int64
}
// 将路径编码为字节
func (path *Path) ToBytes() []byte {
result := []byte{}
// 交替添加代币地址和费率
for i := 0; i < len(path.Tokens); i++ {
// 添加代币地址
result = append(result, path.Tokens[i].Bytes()...)
// 添加费率(如果不是最后一个代币)
if i < len(path.Fees) {
// 将费率转换为3字节
feeBytes := make([]byte, 3)
feeInt := big.NewInt(path.Fees[i])
feeBytes = common.LeftPadBytes(feeInt.Bytes(), 3)
result = append(result, feeBytes...)
}
}
return result
}
对每条可能路径进行询价,选择输出最大(即能兑换出最多目标代币)的路径:
// 询价结果结构
type QuoteResult struct {
AmountOut *big.Int
SqrtPriceX96AfterList []*big.Int
InitializedTicksCrossedList []uint32
GasEstimate *big.Int
}
// 对所有路径询价并找出最优路径
func findBestPath(ctx context.Context,
client *ethclient.Client,
quoterAddress common.Address,
paths []Path,
amountIn *big.Int) (Path, *QuoteResult) {
bestAmount := big.NewInt(0)
var bestPath Path
var bestResult QuoteResult
for _, path := range paths {
// 编码路径
encodedPath := path.ToBytes()
// 调用Quoter合约
result, err := quoteExactInput(
ctx, client, quoterAddress, encodedPath, amountIn)
if err != nil {
continue
}
// 更新最优路径
if result.AmountOut.Cmp(bestAmount) > 0 {
bestAmount = result.AmountOut
bestPath = path
bestResult = *result
}
}
return bestPath, &bestResult
}
基础的路径查找算法已经能够工作,但在生产环境中,我们还需要考虑性能优化和实际应用中的各种问题。
基础路径查找解决了"选哪条路"的问题,但在生产环境中,还有更复杂的优化空间:
对于大额交易,不是把所有资金走一条最优路径,而是智能拆分到多条路径。通过梯度下降算法优化资金分配,让每条路径的边际收益趋于相等,从而最大化总收益。这能为大额交易额外节省5%-15%的成本。
包括热门路径结果缓存、Multicall批量询价、智能路径预排序等工程优化技巧。
这些深度优化涉及复杂的数学建模和系统设计。限于篇幅,具体算法实现就不在这里展开了,你可以让AI帮助你实现。
在我的知识星球中有完整的方案和代码实现,感兴趣的你可以自取。
在实际应用中,还有一些重要的细节需要注意:
流动性实时监控
市场瞬息万变,昨天最优的路径今天可能就不行了。定期检查各个池子的流动性状况,及时调整策略。
错误处理要做好
网络会断,合约会失败,这些都是常事。做好降级方案,比如当主要路径失败时,能快速切换到备用方案。
考虑Gas成本
有时候最省代币的路径需要消耗更多Gas。对于小额交易,Gas费可能比节省的代币还多,得不偿失。
价格波动保护
在高波动市场中,价格可能在你查询和实际交易之间发生大幅变化。设置合理的滑点保护很重要。
并发控制
当很多用户同时查询时,要合理控制对区块链节点的访问频率,避免被限流。
通过这套完整的路径查找系统,你可以:
无论你是在开发DEX聚合器、构建交易机器人,还是为DeFi项目集成最优路径功能,这套技术都能为你的产品带来显著的用户体验提升。
基于全文检索与主题相似度
说在前面 2021年开发Xai-Maker借贷协议时,我们遇到了一个让整个团队头疼的问题:谁来决定借贷利率? 传统金融里,这种事银行说了算。但在DeFi的世界,如果让项目方单方面控制利率,用户凭什么信任你?如果完全放任市场,万一出现极端情况怎么办?经过调研,我把目光投向了Uniswap——这个管理着
在加密世界里,价格波动如同坐过山车,瞬时价格往往并不靠谱。为了更真实地反映市场趋势,时间加权平均价格(TWAP)应运而生:它通过对价格在时间维度上的加权平均,提供一种抗操纵、更稳健的价格参考,广泛应用于清算触发、估值定价等关键场景。 在去中心化金融(DeFi)中,预言机通常承担着链上合约与外部数据之
自17年学习区块链以来,笔者先后参与开发诸多web3项目,如DeFi借贷、NFT交易平台、链游、IDO、O2C、DAO、版权、Fabric等DAPPs,近期想做些知识沉淀与分享,坚持原创,内容持续更新,欢迎关注我的公众号。 本文主要分享Uniswap是怎样做治理的,读完全文大约需要12分钟。 主要内