uniswap_v2
Uniswp是一个去中心化交易所,所谓去中心化,可以从以下两个方面理解:
●交易全部是由开源的代码来控制,没有任何人为的因素
●交易所无实际的掌控机构,规则不能够被随便修改
Uniswap V2 是以太坊上最经典的去中心化交易所 (DEX) 协议之一。它的核心在于彻底抛弃了传统金融的“订单薄” (Order Book) 模式,转而使用了 自动做市商 (AMM) 机制。
简单来说,Uniswap V2 就像一个始终在线的自动机器人或自动售货机:
- 没有买家和卖家:你不是在和另一个人交易。
- 只有资金池:你是在和一个装满代币的智能合约(资金池)进行交换。
- 数学定价:价格不是由人喊出来的,而是由一个简洁的数学公式 $x \cdot y = k$ 自动计算出来的。
AMM (自动做市商) 基础模型
Uniswap V2 的核心就是一个简单的数学公式,它强制规定了合约内两种资产余额的关系。
1. 核心公式:恒定乘积 (The Constant Product)
在传统金融(TradFi)中,价格是由人(买单和卖单)决定的。在 Uniswap V2 中,价格是由状态余额决定的。
公式:
$$x \cdot y = k$$
- $x$: 资金池中 Token A 的余额 (reserve0)
- $y$: 资金池中 Token B 的余额 (reserve1)
- $k$: 恒定常数 (Invariant),也是流动性。在交易过程中(不考虑手续费时),$k$ 必须保持不变。
无手续费的理想状态: 根据恒定乘积公式 x⋅y=k,如果不考虑手续费,输出量 dy 的计算公式为:
$$
dy = \frac{y \cdot dx }{x + dx }
$$
有手续费

🤖 架构师类比:
把 Uniswap 想象成一个“死脑筋的自动售货机”。它不管外面世界 ETH 卖多少钱,它只死守一条规矩:“有人拿走 Token A,就必须放入足够多的 Token B,保证我肚子里的 $A \times B$ 总量不变。”
2. 代码锚点:swap() 的数学推演
这是你作为审计员必须烂熟于心的逻辑。我们来看当用户进行一笔交易时,数值是如何流动的。
假设:
- 用户输入 $\Delta x$ 个 Token A。
- 用户获得 $\Delta y$ 个 Token B。
- 手续费为 $0.3%$ (这意味着只有 $99.7%$ 的 $\Delta x$ 真正进入了池子参与计算)。
推演步骤:
-
交易前状态: $x \cdot y = k$
-
交易后状态: 池子里多了 $\Delta x$ (扣费后),少了 $\Delta y$。为了保持 $k$ 不变:
$$(x + \Delta x_{with_fee}) \cdot (y - \Delta y) = k$$ -
求解 $\Delta y$ (用户能拿走多少钱):
$$\Delta y = y - \frac{k}{x + \Delta x_{with_fee}}$$代入 $k=x \cdot y$,简化后得到 Web3 开发者最眼熟的公式:
$$\Delta y = \frac{y \cdot \Delta x_{with_fee}}{x + \Delta x_{with_fee}}$$
⚠️ 安全警示 (Integer Arithmetic):
在 Solidity 中没有小数。上述除法如果除不尽,会向下取整。Uniswap 的铁律: 所有的取整误差都必须有利于协议(LPs),而不利于交易者。这意味着用户总是少拿哪怕 1 wei 的 Token。如果代码逻辑导致向上取整,可能被攻击者利用“粉尘攻击”逐渐掏空池子。
3.实战模拟
场景设定:
-
池底 (Reserves): 10 ETH ($x$) + 20,000 USDT ($y$)
-
初始价格 (Spot Price): $20,000 / 10 = 2,000$ USDT/ETH
-
任务: 你想买入 1 ETH (把池子里的 ETH 变成 9 个),你需要支付多少 USDT?
计算过程 (Excel 逻辑):
-
目标状态:
池子里的 ETH 必须从 $10$ 变成 $9$。
$$New_ETH_Reserve = 9$$ -
计算新的 $k$ 值 (简化版,先忽略手续费):
$$k = 10 \times 20,000 = 200,000$$ -
计算池子需要多少 USDT 才能平衡 $k$:
$$New_USDT_Reserve = \frac{k}{New_ETH_Reserve} = \frac{200,000}{9} = 22,222.22...$$ -
计算你需要支付的 USDT:
$$Input = New_USDT_Reserve - Old_USDT_Reserve$$
$$Input = 22,222.22 - 20,000 = \mathbf{2,222.22\ USDT}$$
👨💻 架构师分析:
-
理论价格: 买 1 ETH 应该只要 2,000 USDT。
-
实际支付: 2,222.22 USDT。
-
差价: 222.22 USDT。
-
结论: 仅仅买入池子 10% 的流动性,你就承受了 11.1% 的价格冲击!
-
这就是为什么大资金不能在小池子交易。
-
这也是攻击者如何通过“闪电贷”瞬间抽干流动性,人为制造出极其离谱的价格(例如把 USDT 价格拉到 $100),从而去攻击依赖这个价格的借贷协议(预言机操纵攻击)。
-
流动性与 LP Token
在传统世界里,你们会签合同确认股份。但在区块链这个“代码世界”里,没有纸质合同。为了公平地证明每个人在资金池里拥有的“份额”,它会发给你LP Token ,LP Token 就是你在资金池里的“股份证明” (Share)。
无偿损失(Impermanent Loss, IL)
1. 核心类比:消息闭塞的店主
为了理解 IL,我们再次请出我们的 Uniswap 自动售货机。
-
场景: 你是自动售货机的老板(LP)。你在机器里放了 Apple (ETH) 和 Cash (USDT)。
-
设定: 你的机器没有联网。它不知道外面世界苹果卖多少钱,它只认死理($x \cdot y = k$)。
-
事件: 外面菜市场(Binance)的苹果价格突然暴涨了 50%。
发生了什么?
-
套利者 (Arbitrageurs) 发现了机会:你机器里的苹果还卖原来的便宜价!
-
他们疯狂地冲过来,把你机器里便宜的苹果买光,塞给你一大堆现金。
-
直到把你机器里的苹果价格买得和外面一样贵,他们才停手。
结局:
当你晚上来查账时,发现:
-
珍贵的苹果 (ETH) 变少了(被买走了)。
-
贬值的现金 (USDT) 变多了(塞给你了)。
-
总资产: 虽然现金多了,但因为苹果涨得更猛,如果你当初根本不开店,直接拿着苹果睡觉 (HODL),你现在的总资产其实会更高!
这一部分的“少赚的钱”,就是无常损失。
💀 简言之:
无常损失的本质,就是你被迫“低卖高买”。
当币价上涨时,你自动卖出了表现好的资产(ETH);当币价下跌时,你自动买入了表现差的资产(ETH)。你总是在做反向操作。
2. 实战算账:HODL vs. LP
我们用数字来证明这一点。这是所有 DeFi 玩家必须算的一笔账。
初始状态 (Initial):
-
你持有: 10 ETH + 20,000 USDT
-
当前市价: 1 ETH = $2,000
-
总价值: $20,000 + $20,000 = $40,000
突发事件:
ETH 暴涨至 $4,000 (翻倍)。
策略 A:拿住不动 (HODL)
如果你没有把钱存进 Uniswap,而是放在钱包里不动:
-
10 ETH × $4,000 = $40,000
-
20,000 USDT (不变) = $20,000
-
总价值: $60,000
策略 B:提供流动性 (LP)
你把钱存进了 Uniswap。
-
由于套利者的介入,你的池子比例会变(根据 $x \cdot y = k$ 自动调整)。
-
经过计算(为了省事,我直接告诉你结果,公式是 $\sqrt{k/P}$):
-
你的 ETH 会剩下约 7.07 个 (被买走了 2.93 个)。
-
你的 USDT 会变成 28,284 个。
-
-
现在价值:
-
(7.07 ETH × $4,000) + 28,284 USDT
-
$28,280 + $28,284 = $56,564
-
惨痛对比
-
HODL 躺平: $60,000
-
辛辛苦苦当 LP: $56,564
-
无常损失: $60,000 - $56,564 = $3,436 (亏了约 5.7%)
结论:
ETH 涨了,你虽然赚了(从 4万 变成了 5.6万),但你跑输了大盘。如果算上你在做 LP 期间赚的手续费,如果不超过 $3,436,那你其实是亏本的。
滑点与价格冲击
滑点就是:“你预期的价格” 和 “实际成交的更差价格” 之间的差额。
“价格冲击” (Price Impact):因为你交易量太大、导致价格向你不利方向移动的现象 。

这就是传说中的 Uniswap V2 联合曲线 (Bonding Curve)。这张图不仅仅是一条数学曲线,它是 DeFi 世界的“物理定律”。
-
绿点 (Start):
这是我们的初始状态。池子里有 10 ETH 和 20,000 USDT。
($10 \times 20,000 = 200,000$) -
红点 (End):
这是你刚才“大额买入 5 ETH”后的状态。
你看,为了维持 $k$ 不变,当你把 ETH 的库存从 10 买到剩 5 时,USDT 的库存被迫从 20,000 飙升到了 40,000(你需要支付 20,000 USDT!)。 -
红色的箭头:
这就是“交易”。
在 Uniswap 里,交易不是握手,而是滑滑梯。
你只是推动池子的状态点,沿着这条光滑的曲线移动。- 买入 ETH = 向左上方移动(ETH 变少,USDT 变多)。
- 卖出 ETH = 向右下方移动(ETH 变多,USDT 变少)。
实战算账:深池 vs. 浅池的数值推演
根据你的 Checklist,我们要计算在不同 TVL (Total Value Locked) 下,购买等量资产的惨烈对比。
场景设定:
-
你的任务: 购买 10,000 USDT 等值的 ETH。
-
当前价格: 1 ETH = 2,000 USDT。
🦈 场景 A:浅池子 (Shallow Pool)
-
池子状态: 10 ETH + 20,000 USDT ($k = 200,000$)。
-
池子总价值: $40,000 (Very Low)
-
你的行为: 投入 10,000 USDT 购买 ETH。
计算:
-
投入前: $y = 20,000$。
-
投入后: $y_{new} = 30,000$。
-
新的 ETH 余额: $x_{new} = k / 30,000 = 200,000 / 30,000 = 6.66$ ETH。
-
你买到的 ETH: $10 - 6.66 = \mathbf{3.33\ ETH}$。
-
实际成交价: $10,000 / 3.33 = \mathbf{3,003\ USDT/ETH}$。
-
价格冲击: $(3003 - 2000) / 2000 \approx \mathbf{50%}$。
- 你直接把价格买崩了 50%!亏损一半本金。
🐋 场景 B:深池子 (Deep Pool)
-
池子状态: 10,000 ETH + 20,000,000 USDT ($k = 2 \times 10^{11}$)。
-
池子总价值: $40,000,000 (High)
-
你的行为: 投入 10,000 USDT 购买 ETH。
计算:
-
投入前: $y = 20,000,000$。
-
投入后: $y_{new} = 20,010,000$。
-
新的 ETH 余额: $x_{new} = k / 20,010,000 \approx 9,995.002$ ETH。
-
你买到的 ETH: $10,000 - 9,995.002 = \mathbf{4.998\ ETH}$。
-
实际成交价: $10,000 / 4.998 \approx \mathbf{2,000.8\ USDT/ETH}$。
-
价格冲击: $(2000.8 - 2000) / 2000 = \mathbf{0.04%}$。
- 几乎无感。
安全工程师的“黑暗森林”视角
为什么要特意算这个?因为 Flash Loan (闪电贷)。
黑客没有几千万美金的本金,但他可以借。
如果黑客借来 1 亿美金,对着一个中等深度的池子(比如某个山寨币池子)砸进去:
-
制造价格冲击: 瞬间把价格从 $1 拉升到 $100。
-
外部获利: 在另一个借贷协议(比如 Aave 的分叉版)里,用这个山寨币做抵押。
-
借贷协议的预言机看到了 Uniswap 的价格 $100,误以为这个币很值钱。
-
借空资金: 允许黑客借出巨额的 ETH/USDT。
-
跑路: 黑客还掉闪电贷,带着借出来的真金白银消失。
这就是 Oracle Manipulation Attack 的核心原理:利用资金量制造 Price Impact,扭曲现实。
Code Anchor: getAmountsOut
在代码里,Router 合约会帮你计算这笔账。作为开发者,你必须知道这个函数。
// UniswapV2Router02.sol
function getAmountsOut(uint amountIn, address[] memory path)
public
view
returns (uint[] memory amounts)
{
// ... 前置逻辑 ...
// 这里就是核心:根据输入金额,递归计算能换出多少钱
// 它使用的是 getAmountOut 函数(基于 xy=k)
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
安全审计点:
如果一个 DeFi 协议直接依赖 getAmountsOut 的返回值作为“价格预言机”,那么它100% 会被黑掉。因为 getAmountsOut 反映的是当前瞬间的价格,极易被闪电贷操纵。
组成
在 Uniswap V2 协议中,为了实现去中心化交易和自动做市商(AMM)的功能,整个系统主要由三个核心合约组成:Factory(工厂合约)、Pair(配对合约) 和 Router(路由合约)。
这三个合约分工明确,分别负责资产管理、合约部署和用户交互。
1. Factory Contract(工厂合约)
Factory 合约的主要职责是生成和索引 Pair 合约。它是整个协议的“入口”,确保每个代币对只有唯一的合约地址。
- 核心功能: 它的主要任务是部署 Pair 合约。如果某个代币对(例如 DAI/ETH)还不存在,任何人都可以调用 Factory 合约来创建它。
- 部署机制: Factory 使用
create2操作码来部署 Pair 合约。这允许在合约实际部署之前就能确定性地计算出 Pair 合约的地址,。 - 注册与索引: 一旦部署完成,Factory 会将新生成的 Pair 合约地址记录在
getPair映射中,防止重复创建,。
2. Pair Contract(配对合约)
Pair 合约是 Uniswap V2 的核心逻辑所在,属于 v2-core 代码库。每一个交易对(例如 ETH/USDT)都有一个独立的 Pair 合约。
- 资金托管: Pair 合约直接持有该交易对的两种 ERC20 代币资产(Reserves),。
- 核心逻辑: 它实现了恒定乘积公式($x \times y = k$)的核心数学逻辑。它负责处理底层的
mint(添加流动性)、burn(移除流动性)和swap(交换代币)操作,。 - LP 代币: Pair 合约本身也是一个 ERC20 合约。当流动性提供者(LP)向其中存入资金时,Pair 合约会铸造代表份额的 LP 代币给用户,。
- 设计原则: Pair 合约的设计非常精简且注重安全性,它不建议用户直接调用。因为直接与 Pair 交互容易因操作失误导致资金锁死或丢失(例如直接发送代币而未调用 swap 函数),。
3. Router Contract(路由合约)
Router 合约是用户与 Uniswap 交互的中介,属于 v2-periphery 代码库。它的存在是为了保护用户并简化操作。
- 用户接口: 由于 Pair 合约设计得过于底层和危险,Router 合约充当了“中间人”。用户在前端界面操作时,实际上是在调用 Router 合约,再由 Router 去调用 Pair 合约。
- 多跳交易(Multi-hop Swaps): Pair 合约只能处理两个特定代币之间的交换。如果用户想用 A 换 C,但只有 A/B 和 B/C 的交易对,Router 会自动规划路径(A->B->C),连续调用多个 Pair 合约完成交易,。
- 安全辅助: Router 包含了很多辅助检查机制,例如滑点保护(Slippage protection)和交易截止时间(Deadline),确保代币能安全地从用户账户转移到 Pair 合约中,。
总结:三者的协作流程
为了更直观地理解这三个合约的关系,我们可以看一个典型的用户操作流程:
- 部署阶段: 开发者或用户调用 Factory 合约部署一个新的 Pair 合约。
- 交易阶段:
- 用户想要交易(例如用 ETH 买 DAI),他们会调用 Router 合约的
swapExactETHForTokens函数。 - Router 帮助用户将 ETH 转换为 WETH(因为 Pair 只处理 ERC20),并将资金安全传输给 Pair 合约。
- Pair 合约接收资金,根据 $x \times y = k$ 计算出应得的 DAI 数量,并将 DAI 发送给用户,。
- 用户想要交易(例如用 ETH 买 DAI),他们会调用 Router 合约的
简而言之,Factory 是生成器,Pair 是资金池和数学引擎,而 Router 是用户的操作界面。

浙公网安备 33010602011771号