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$ 真正进入了池子参与计算)。

推演步骤:

  1. 交易前状态: $x \cdot y = k$

  2. 交易后状态: 池子里多了 $\Delta x$ (扣费后),少了 $\Delta y$。为了保持 $k$ 不变:
    $$(x + \Delta x_{with_fee}) \cdot (y - \Delta y) = k$$

  3. 求解 $\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 逻辑):

  1. 目标状态:
    池子里的 ETH 必须从 $10$ 变成 $9$。
    $$New_ETH_Reserve = 9$$

  2. 计算新的 $k$ 值 (简化版,先忽略手续费):
    $$k = 10 \times 20,000 = 200,000$$

  3. 计算池子需要多少 USDT 才能平衡 $k$:
    $$New_USDT_Reserve = \frac{k}{New_ETH_Reserve} = \frac{200,000}{9} = 22,222.22...$$

  4. 计算你需要支付的 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%。

发生了什么?

  1. 套利者 (Arbitrageurs) 发现了机会:你机器里的苹果还卖原来的便宜价!

  2. 他们疯狂地冲过来,把你机器里便宜的苹果买光,塞给你一大堆现金。

  3. 直到把你机器里的苹果价格买得和外面一样贵,他们才停手。

结局:

当你晚上来查账时,发现:

  • 珍贵的苹果 (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。

计算:

  1. 投入前: $y = 20,000$。

  2. 投入后: $y_{new} = 30,000$。

  3. 新的 ETH 余额: $x_{new} = k / 30,000 = 200,000 / 30,000 = 6.66$ ETH。

  4. 你买到的 ETH: $10 - 6.66 = \mathbf{3.33\ ETH}$。

  5. 实际成交价: $10,000 / 3.33 = \mathbf{3,003\ USDT/ETH}$。

  6. 价格冲击: $(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。

计算:

  1. 投入前: $y = 20,000,000$。

  2. 投入后: $y_{new} = 20,010,000$。

  3. 新的 ETH 余额: $x_{new} = k / 20,010,000 \approx 9,995.002$ ETH。

  4. 你买到的 ETH: $10,000 - 9,995.002 = \mathbf{4.998\ ETH}$。

  5. 实际成交价: $10,000 / 4.998 \approx \mathbf{2,000.8\ USDT/ETH}$。

  6. 价格冲击: $(2000.8 - 2000) / 2000 = \mathbf{0.04%}$。

    • 几乎无感。

安全工程师的“黑暗森林”视角

为什么要特意算这个?因为 Flash Loan (闪电贷)

黑客没有几千万美金的本金,但他可以借。

如果黑客借来 1 亿美金,对着一个中等深度的池子(比如某个山寨币池子)砸进去:

  1. 制造价格冲击: 瞬间把价格从 $1 拉升到 $100。

  2. 外部获利: 在另一个借贷协议(比如 Aave 的分叉版)里,用这个山寨币做抵押。

  3. 借贷协议的预言机看到了 Uniswap 的价格 $100,误以为这个币很值钱。

  4. 借空资金: 允许黑客借出巨额的 ETH/USDT。

  5. 跑路: 黑客还掉闪电贷,带着借出来的真金白银消失。

这就是 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 合约中,。

总结:三者的协作流程

为了更直观地理解这三个合约的关系,我们可以看一个典型的用户操作流程:

  1. 部署阶段: 开发者或用户调用 Factory 合约部署一个新的 Pair 合约。
  2. 交易阶段:
    • 用户想要交易(例如用 ETH 买 DAI),他们会调用 Router 合约的 swapExactETHForTokens 函数。
    • Router 帮助用户将 ETH 转换为 WETH(因为 Pair 只处理 ERC20),并将资金安全传输给 Pair 合约。
    • Pair 合约接收资金,根据 $x \times y = k$ 计算出应得的 DAI 数量,并将 DAI 发送给用户,。

简而言之,Factory 是生成器,Pair 是资金池和数学引擎,而 Router 是用户的操作界面。

posted @ 2026-02-01 10:47  Ma&0xFly  阅读(2)  评论(0)    收藏  举报