Summary

目前Uniswap是Defi现货交易的一个重要基础,只要你想接触区块链就会碰到它,并且它的设计范式是被整个行业参考和借鉴。从2018年开始,截止到目前为止一共更新了四个版本,每个版本都有自己的特性,接下来我们逐一讲解

V1

V1版本是Hayden Adams工程师用Vyper(pyhton版的evm语言)实现的一版,主要证明了

  • 恒定乘积AMM是能跑的
  • 引入了 LP token概念
  • 不需要对手方也可以成交的

核心公式就一个 x * y = k

这个版本也有很强烈的限制,比如

  • 仅支持ETH<-> ERC20池,任意一个token必须和ETH做对
  • 手续费固定0.3%
  • 没有价格预言机

这些问题也催生出了V2

V2

V2版本于2020年,它保留了V1的 x * y = k公式,在生态层面作出了几个关键升级。

任意ERC20/ERC20池

放弃了V1必须走ETH的路线,允许任意两种ERC20直接组池。

这个变化的连锁反应超出预期:

  • 稳定币直兑池爆发——USDC/USDTDAI/USDC 池深度很快超过 CEX
  • 新代币发行更灵活——项目方发币时可以直接和自己的稳定币建池,不必先兑 ETH
  • 长尾市场繁荣——SUSHI/YAM、YFI/YFV 这些 DeFi meme 时代的核心资产都在 V2 上诞生

TWAP预言机

在每一个pair合约(一个交易对,USDC/WETH)中记录一个累积价格,每次swap时进行更新,

priceCumulativeLast += 当前价格 * 时间间隔

例如:在T1时刻记录一次priceCumulativeLast,T2时刻再记一次,获取时,者着相减/时间差,得到T1到T2之间的时间加权平均价格

举个例子:

时间事件价格该价格持续时长
0s开始观察$2000600s
600s有人买入 ETH涨到 $2100300s
900s有人卖出 ETH跌到 $1900900s
1800s结束观察

累积价格的变化过程(假设 0 秒时读到的值是 0):

600s 时刻的 swap 触发更新:

累积价格 += 2000 × 600 = 1,200,000

900s 时刻的 swap 触发更新:

累积价格 += 2100 × 300 =   630,000  → 累计 1,830,000

1800s 时刻我读取(触发更新):

累积价格 += 1900 × 900 = 1,710,000   → 累计 3,540,000

套入公式:

TWAP = (结束时累积价格 - 开始时累积价格) / 时间差 = (3,540,000 - 0) / 1800 = $1966.67

这种机制可以有效的防止被闪电贷在单块内操纵价格

手续费开关(Fee switch)

V1中所有的手续费都给了LP,在V2中引入了一个开关,这个开关通过治理打开,一打开,0.3%手续费中的1/6就会被切到协议金库,这个开关,好像从上线开始到现在一直没有打开过,直到2024年,才在部分池子中开启。

合约Core/Periphery分离

V2 将整体合约分为Core/Periphery两部分,Core包含

  • UniswapV2Factory(所有的pair都有此创建)
  • UniswapV2Pair (所有的钱都在这里)
  • UniswapV2ERC20(Pair继承的LP Token的实现,支持permit离线签名操作)

作为核心,极简、不可升级、并且管钱

Periphery(外围合约)包括

  • UniswapV2Router02 (用户的实际入口,多跳路径规划、滑点保护等功能都在这里)
  • UniswapV2Library (工具库)

这种设计让核心合约(pair)保持极简,业务逻辑放在 Router。Router 可以升级、可以被别的协议替换。这个"Core / Periphery"分离思想被后续所有 Uniswap 版本沿用。

V3

V3是在2021年上线的,这次升级让pool的资本效率提升了100倍以上,同时对散户的门槛也提高了,是个对散户不是很友好的版本。

CLMM(集中流动性)

在一个 USDC/USDT 池子中。这两个稳定币的价格几乎永远在 $0.999 ~ $1.001 之间波动。但 V2 的公式是 x * y = k——流动性均匀分布在 0 到 ∞ 的所有价格区间,例如下图:

流动性密度
     │ ━━━━━━━━━━━━━━━━━━━━━━━  ← V2:每个价格上摆的流动性都一样(均匀分布)
     └──────────────────────── 价格
     0        1              ∞

在以前情况,池子都要保持流动性

  • 价格 = 0.5 时:需要有钱(万一价格砸到那)
  • 价格 = 2 时:需要有钱(万一价格涨到那)
  • 价格 = 10、100、10000 时:也都得有钱

结果就是:99.9% 的流动性趴在永远不会被用到的价格区间。你放 100 万美元进 USDC/USDT V2 池,真正在 $0.999-$1.001 之间被使用的可能只有 1000 美元。资本效率 = 0.1%,证明过程如下,包含挺多的数学公式,不想看的可以跳过:

证明:
  第一步:把池子状态用公式表示

  V2 公式:x * y = k(x = USDC 数量,y = USDT 数量)

  价格定义:p = y/x(1 个 USDC 值多少 USDT)

  由这两条可以解出,任意价格 p 时池子里的资产量:

  x = √(k/p)        y = √(k·p)

  第二步:代入"100 万美元、价格 = 1"

  价格 p = 1 时,x = y = √k,池子总价值:

  总价值 = x + y = 2√k = $1,000,000
  所以 √k = 500,000
  即池子里:50 万 USDC + 50 万 USDT

  第三步:算"价格从 1.001 走到 0.999,池子里实际动了多少钱"

  "被用到的资金" = 价格在这条缝里来回时,真正参与成交(被换进换出)的资产量。

  价格 p = 1.001 时:
  y = √k × √1.001 = 500,000 × 1.00049988 ≈ 500,250 USDT

  价格 p = 0.999 时:
  y = √k × √0.999 = 500,000 × 0.99949987 ≈ 499,750 USDT

  USDT 的变动量:
  Δy = 500,250 − 499,750 ≈ 500 USDT

  同样算 USDC(x = √(k/p)):
  p=0.999 时 x ≈ 500,250,  p=1.001 时 x ≈ 499,750
  Δx ≈ 500 USDC

  第四步:得出结论

  价格在 $0.999 ↔ $1.001 之间扫完整个区间,池子里真正被换手的资产:

  Δx + Δy ≈ 500 + 500 = $1,000

  资本效率:
  1,000 / 1,000,000 = 0.1%

  其余 99.9 万美元,无论价格在这条缝里怎么震,一动不动 —— 它们只有在价格跑出这个区间时才会被动用。

V2的资金利用率就好像你开了一家只做午市的餐厅,但你雇了 100 个服务员 24 小时三班倒站岗 —— 半夜 3 点也有很多服务员站在空餐厅里,做服务的永远只有午市的那几个服务员,剩下的服务员工资都白给了。

V3出来之后,会让提供流动性之前让你自己选择一个价格区间,同样 100 万美元产生的有效流动性相当于 V2 的 ~500 倍(因为流动性密度提升了)。但是同时也增加了LP的工作,你需要主动管理LP,当LP中只有单边时,会停止赚手续费,需要你手动调整,每次调整还需要付gas,还可能会重新开仓,所以散户可以直接放弃这个版本了。

Fee Tier

以前V2中,手续费固定0.3%,V3则提供4档

Fee Tier目标资产类型
0.01%稳定币互换(USDC/USDT、DAI/USDC)
0.05%稳定币和挂钩资产(stETH/ETH)、蓝筹(USDC/ETH)
0.30%中等波动资产(ETH/大部分 ERC20)
1.00%高波动 / 长尾资产

Tick系统

Tick

这个是为了解决LP任意边界而产生的,先看没有Tick的世界有多糟

假设允许任意边界,池子里有三个 LP:

张三: [0.99871, 1.00133] 放了 10 万
李四: [0.99902, 1.00087] 放了 10 万
王五: [0.99913, 1.00121] 放了 10 万

现在价格从 1.0008 涨到 1.0009。合约必须回答一个问题:“这一步有没有跨过谁家的边界?”

怎么回答?只能把所有仓位挨个查一遍:张三的上边界 1.00133 过了吗?李四的 1.00087 过了吗?王五的呢……池子里有 1 万个 LP,就查 1 万次。每笔 swap 都这么查,gas 天文数字。

问题的根源:每个人的边界都是"私有的",合约必须认识每一个仓位(这个问题是不是很像开发过程中,你需要遍历所有的数组的情况),在EVM中每次读取一次数据,会有2100的gas,如果每一个仓位都要读取的话,那gas消耗就不是一般的大了。

解决:价格的边界只能选在预设刻度上(tick)。于是:

张三: [tick -13, tick +13] (计算公式: tick = ln(price) / ln(1.0001), 为什么用1.0001,是因为 log_1.0001(x) 让每个 tick 之间的价格差 ≈ 0.01%(1 basis point)——这是 CEX 交易员熟悉的最小 tick size) 李四: [tick -10, tick +9] 王五: [tick -9, tick +12]

关键变化发生了 —— 边界的"种类"变得有限了。张三、李四、王五各自的钱不同、区间不同,但他们的边界都落在 -13, -10, -9, +9, +12, +13 这六根钉子上。

于是合约可以换一种记账方式:不给"人"记账,给"钉子"记账。

每根被使用的钉子上挂一个数字,叫 liquidityNet(流动性净变化):

钉子 -13: +10万 ← 价格从下往上跨过这里时,张三的钱上岗 钉子 -10: +10万 ← 李四的钱上岗 钉子 -9: +10万 ← 王五的钱上岗 钉子 +9: -10万 ← 李四的钱下岗 钉子 +12: -10万 ← 王五的钱下岗 钉子 +13: -10万 ← 张三的钱下岗

现在价格上涨跨过钉子 +9 时,合约根本不需要知道李四是谁,它只做一个动作:

当前在岗流动性 = 30万 + (-10万) = 20万

一次加法,完事。1 万个 LP 和 3 个 LP,跨钉子的成本一模一样 —— 因为所有人的边界都被"合并同类项"到了有限的钉子上。

这就是不允许边界,预设刻度的全部含义,让合约按tick记账,而不是按照人来记账(你的明细存放在你自己的仓位NFT上)。

新问题:

按照tick来记账,怎么才能快速的找到下一个tick呢,V3的tick范围是 -887272 到 +887272,约 177 万根钉子。但刚才的池子里只有 6 根钉子有流动性,其余177多万根全是空的,那要怎么才能快速找到呢。

V3使用Bitmap:用 1 个比特标记 1 根钉子

解法是给全部钉子建一张位图(bitmap) —— 每根钉子对应 1 个比特:

该钉子挂了账(被初始化) → 1 该钉子是空的 → 0

以太坊的一个存储槽是 256 位,正好可以打包 256 根钉子的状态:

槽 0 管 tick 0~255: 0000000001000000…0010000000… ↑ ↑ tick 9 = 1 tick 12 = 1

现在"找下一根有账的钉子"变成了:

① 读 1 个存储槽(拿到 256 根钉子的 0/1 状态) ← 1 次读,2100 gas ② 用位运算在这 256 位里找下一个 1 ← 纯计算,几乎免费

一次读取覆盖 256 根钉子。就算下一根有账的钉子在几千格外,也只需要读十几个槽,而不是几千次。这就是 bitmap 的全部作用:把"大海捞针式的逐格扫描"压缩成"一页页翻目录"。

Tick spacing(间距)

tick一共有177多万根钉子,如果每一根都使用,那算下来也是一笔很大的gas消耗,所以针对不同的代币,使用不用的spacing,例如:

  • 主流币池(0.30% 费率, spacing = 60),每 60 根钉子才有一根能用
  • 长尾币池(1.00% 费率, spacing = 200),每 200 根钉子才有一根能用

V4

V4于2025年1月上线,代号Hooks + Singleton。这一版主要是让Uniswap从"一个协议"变成了"一个可编程 DEX 引擎"。

Hooks - 核心

Hooks 让每个池子可以挂一个外部智能合约作为"回调",在特定 swap 生命周期节点被自动调用:

Hook 时点触发时机
beforeInitialize池子创建前
afterInitialize池子创建后
beforeAddLiquidityLP 添加流动性前
afterAddLiquidityLP 添加流动性后
beforeRemoveLiquidityLP 移除流动性前
afterRemoveLiquidityLP 移除流动性后
beforeSwapswap 前
afterSwapswap 后
beforeDonate捐赠前
afterDonate捐赠后

有了 Hook,你可以在同一个 Uniswap V4 引擎上实现:

  • 动态手续费beforeSwap 里根据市场波动率调整 fee(这个也是路由服务里面一个重要的工程)
  • 限价单afterSwap 里检查当前价格,达到用户预设条件时自动执行
  • KYC 池beforeSwap 里验证调用者是否在白名单
  • 自定义预言机afterSwap 里更新自己的 TWAP 存储
  • MEV 保护beforeSwap 里检测 sandwich 并拒绝
  • 激励分发afterAddLiquidity 里给 LP 空投别的代币

这就是 “DEX 变平台"的意思——之前想做这些事得部署一个新的 DEX(Bancor 特殊设计、Balancer 加权池、Curve StableSwap)。现在都可以是V4 上的一个 Hook 合约

Singleton架构

V2/V3 里,每个 pair 都是独立部署的合约。以太坊主网上有数万个 V2/V3 pair 合约。这意味着:

  • 每次跨多个池子路由,都要跨合约调用(贵)
  • 部署新池子要花 gas
  • 数据是碎片化的(要查 100 个池子的状态得读 100 个合约)

V4 把所有池子塞进一个合约(PoolManager),池子只是这个合约里的一个数据结构(storage)。

好处:

  • 创建新池子 gas 降低 99%(从部署合约变成写几个 storage slot)
  • 多跳 swap 只需要一次合约调用
  • 支持"闪电记账”(flash accounting)——一次交易内的多个 swap 可以只做净结算,中间状态不落地

Uniswap X

在跨链场景中,如果你想swap 10000USDC -> ETH,传统流程是,调用Router计算最优路径,签一笔交易,支付gas,承担滑点,并且自己防范MEV(三明治)攻击。

而Uniswap X的思路是,用户不再签"我要在池子 X 用路径 Y swap",而是签"我要用最多 10000 USDC 换到不少于 5.5 ETH"(intent)。

然后Filler 网络,会去竞争这个intnet,流程如下:

  1. 你签一个 EIP-712 结构化订单,广播到 Uniswap X orderflow 网络
  2. 各个 filler 在链下算怎么最便宜地填充你(他们可能用自己的库存、可能路由到 Uniswap V4、可能路由到 CEX 库存桥回来)
  3. 出价最高的 filler 中标,链上执行,并且无MEV
  4. 你付 0 gas(filler 付),拿到承诺的最低金额

底层机制: filler(做单人)在目标链先垫付你要的币,再回源链领走你付的币 —— 由 The Compact(资源锁)做安全结算

目前这个版本已经上线,最新的动态是已经在Robinhood Chain上线。

PS:这个感觉就很像广告中的RTB。

最后说一句

感觉区块链就是数字的游戏,里面的包含太多的数学知识了