第三层第一篇——DeFi 开篇。01–09 装好了"账本 + EVM + 合约 + 代币"四件套。 这一篇答为什么链上能 7×24 跑一台"自动做市"机器、报价从哪来、滑点的本质是什么、 为什么我们 12/12 fork 测试能 wei-exact 验证套利结果。 沿用 01–09 的村庄画面。结构见 README.md §1。


0. 一句话本质

AMM(Automated Market Maker)= 用一个数学函数代替"做市商"角色的合约: pool 里两边代币(reserves)满足某条曲线(V2 = x·y = k),你换出多少由曲线 精确算出,无需对手方挂单、不依赖外部预言机喂价;价格是当前 reserves 比的一个衍生量, 不是"存"在 pool 里的某个字段。这是 DeFi 的水管,也是我们闪电贷套利能存在的物理基础。


1. 怎么产生的:它解决什么问题

01–09 解决了"账本 + 货币",但还有一个具体落地缺口:

  1. Phase 1 我们看 CEX 用"订单簿 + 做市商"撮合:链上能不能也搞订单簿? ⇒ 理论可以,实际太贵:每一笔挂单、改单、撤单都是一笔 tx,gas 烧死人。 早期 EtherDelta / 0x v1 尝试过订单簿模式,全部被 AMM 模式吃干净。
  2. 没有做市商挂单,那 swap 的对手方在哪?价格谁定?没有对手方,对手方是 pool 本身。pool 里两种代币的 reserves 通过曲线绑死, 你存入 Δx,按曲线算出 Δy 给你。价格 = 当前 reserves 的比,完全内生
  3. 谁愿意往 pool 存币?为什么不全被人套走? ⇒ LP(流动性提供者)。LP 存等价值的两种代币换 LP token,享受每次 swap 收的 0.3% 手续费。 swap 收的费留在 pool 里,间接抬高 LP token 占的 reserves 份额。
  4. 既然报价完全内生,那链上价格怎么"知道" 1 ETH 大致等于 3000 USDC? ⇒ 不"知道"。任何 pool 都允许偏离外部价。套利者发现偏离就来做反向 swap → reserves 重新平衡 → pool 价向外部市场价回归。这就是 MEV 套利的源头。
  5. 为什么我们 Arbitrageur test 能 wei-exact 通过?换算公式存在浮点误差吗? ⇒ 没有浮点。V2 的 getAmountOut纯整数运算(含 fee = 997/1000 用整数实现)。 给定相同 reserves + amountIn,链上和我们 TypeScript 算的完全相同的 256-bit 整数。 这是 12/12 wei-exact 测试群的可能性根。
  6. 0.3% 手续费收给谁?协议? ⇒ V2:全部给 LP(写进 reserves,没有单独的 fee 账户)。 V3:可选打开"protocol fee"给 Uniswap 治理金库,但默认关闭,大头仍归 LP。
  7. Uniswap V2 和 Sushi 是两个生态吗?为什么我们的 Arbitrageur 在两者之间套利?Sushi 是 V2 的 fork,代码 100% 相同,只换了 factory 地址 + token logo。 所以两者的 pair 计算 getAmountOut 的公式完全一致;二者价格不同唯一原因是 reserves 不同(流动性深度、最近哪笔大单进过来),套利就成立。

→ 这一篇答全部。


2. 它本质在做的"那一件事"

把"价格发现 + 做市"从一群人(CEX 的做市商军团)压缩到一个数学函数 + 一池资金: 任何人可成为 LP(投钱接住别人 swap)、任何人可成为 trader(拿对应代币按曲线换出对手币), 全部 24/7 自动运行、不可被审查、price 完全由当前状态衍生。 代价是:曲线上每一笔交易必然引入滑点;价格回归外部市场要靠套利者,套利者就是吃肉的那群人。

→ 这就是为什么我们 Phase 4 反复观察到"V2-V2 残差为 0"——套利者已经把每个偏离一扫而光(自我更正记录里强调过: 零残差 = footprint,不是没机会)。


3. 类比:村庄"自动找零桶"

01–09 的村庄已经有了 USDC、WETH 这种代币(09 代金券系统)。10 加一类新设施:

┌── 自动找零桶(一份 Uniswap V2 Pair 合约)──────────────────┐
│                                                            │
│  桶里两侧装的是两种代币:                                  │
│    左侧:USDC reserves x(比如 10,000,000 USDC)           │
│    右侧:WETH reserves y(比如 3,000 WETH)                │
│                                                            │
│  桶有个铁律:左 × 右 = k(一个常数)                       │
│    10,000,000 × 3,000 = 30,000,000,000  ← 此即 k          │
│                                                            │
│  你来换:丢 26,869 USDC 进左侧                             │
│    桶按公式算给你右侧应付多少 WETH,使得新的乘积 ≥ k       │
│    (扣 0.3% 手续费,剩余 0.997 那部分参与计算)           │
│                                                            │
│  桶不需要:                                                │
│    - 外部喂价(价格 = y/x 完全内生)                       │
│    - 对手方(桶自己就是对手)                              │
│    - 中央做市商(曲线就是做市策略)                        │
│                                                            │
│  桶承诺:                                                  │
│    - 谁来都同一公式(人人公平)                            │
│    - 24/7 永不关门(合约不会下线)                         │
│    - 0.3% fee 全留桶内(LP 平分)                          │
│                                                            │
│  桶不承诺:                                                │
│    - 报价跟外部市场一致(偏离就让套利者来收割)            │
│    - 大单不滑(trade size 越大,曲线弯得越厉害)           │
└───────────────────────────────────────────────────────────┘

→ 一图说尽 AMM 全部精髓。"价格"是 y/x 比;"成交"是改两边 reserves;"手续费"是悄悄抬高 k。


4. 核心关键点(5 条)

  1. 恒定乘积 x · y = k(V2)

    pool 里 x = 代币 A reserves、y = 代币 B reserves;交易前 x·y = k_before。 一笔无手续费的交易在曲线上滑:从 (x, y) 滑到 (x+Δx, y−Δy),要求 (x+Δx)·(y−Δy) = k_before。 ⇒ Δy = y · Δx / (x + Δx)

  2. V2 getAmountOut 精确公式(含 0.3% fee)

    amountInWithFee = amountIn * 997
    numerator       = amountInWithFee * reserveOut
    denominator     = reserveIn * 1000 + amountInWithFee
    amountOut       = numerator / denominator   ← 整数除法,向下取整
    
    • 997/1000 ⇔ 扣 0.3% 手续费(fee 留 pool 内 → LP 共享)
    • 全是 uint256 整数运算,没有浮点 → 我们 TS / Solidity / Python 算出同一字节结果
    • 真正的不变量不是 k,而是 (x + 0.997·Δx) · (y − Δy) ≥ x · y——k 在每笔交易后略微变大
  3. 价格 = 衍生量,不是字段

    • Spot price(即时价)= reserveOut / reserveIn,告诉你"无穷小一笔交易"的瞬时汇率
    • Effective price(实际成交价)= amountOut / amountIn,永远比 spot 差(滑点)
    • 大单情况下 effective 远低于 spot;这就是"价格冲击"(price impact)
  4. 滑点 / 价格冲击 = trade size / pool depth

    • 小单(amountIn ≪ reserveIn):effective ≈ spot,几乎无滑
    • 大单(amountIn 接近或超过 reserveIn 量级):effective 严重恶化
    • 决定性变量是池深——同样换 1 ETH,深池 0.01% 滑,浅池可能 5% 滑
    • 设置 slippage tolerance 是把"最低可接受 amountOut"传给 router,pool 给的低于这个数就 revert
  5. LP / 0.3% fee / V2 vs V3 / fork 链

    • LP(Liquidity Provider):往 pool 存等价值两种代币 → 拿 LP token → 占池子 share
    • 每笔 swap 0.3% 留 pool 内:不分给单独 fee 账户,而是悄悄抬高 reserves,间接 inflate 每张 LP token 背后的资产
    • V3 是 V2 大改版:引入"集中流动性"——LP 可指定价格区间,但本质仍是分段恒定乘积; V3 还有 0.05% / 0.3% / 1% 多档 fee tier。本项目用 V2 因为公式简单可 wei-exact 推导
    • Sushi = Uniswap V2 fork:代码 100% 同;只换 factory + ui。我们 Arbitrageur 跨 Uni V2 / Sushi 套利就是 fork 链同质化的副产物

5. 必须先认识的前置名词(小词典)

名词 一句话 类比
DEX 去中心化交易所;这里特指 AMM 类(V2/V3/Curve/Balancer) 自动找零桶集群
AMM 自动做市商,按曲线报价;V2 是最经典 自动找零桶
pool / pair 装两种代币 reserves 的合约(V2 里一对就是一个合约) 一只桶
reserves pool 里两种代币的当前余额 (x, y) 桶左右的水位
constant product / x·y=k V2 维持的不变量 桶的铁律
getAmountOut 给 amountIn + 两边 reserves → 算 amountOut 桶的找零公式
spot price 瞬时单价 = reserveOut/reserveIn "理论标价"
effective price 实际成交单价 = amountOut/amountIn 真到手的汇率
slippage / price impact spot 和 effective 的差 大单买东西被坑的部分
LP(流动性提供者) 往 pool 存币的人 桶的合伙人
LP token 凭证;持有比例 = 占 pool share 合伙人股份证
fee tier V2 固定 0.3%;V3 可选 0.05/0.3/1% 桶的运营费率
router 帮你撸多 hop swap 的合约(V2 = 0x7a25…488D,Sushi = 0xd9e1…E7F swap 的导航员
pair address V2 里两 token 决定一个固定地址:keccak256(factory + token0 + token1 + initcode)[:20] 找零桶的固定门牌
factory 部署 pair 的工厂合约 桶厂
Uniswap V2 vs V3 V2 全曲线 / V3 集中流动性 + 多 fee tier 单档桶 / 多档桶
MEV / 套利者 把 pool 价拉回外部市场的人 自动均价的"水位调度员"

6. 一个完整过程走一遍:26,869 USDC → WETH on Uni V2

我们 Arbitrageur 在 demo 里就是这种 swap(带闪电贷)。把数学走完整:

设定

  • pool: Uniswap V2 USDC-WETH 0xB4e1...5640
  • 当前 reserves(示例值):
    • reserveIn = 50,000,000 × 10^6 (= 5e13 base units USDC)
    • reserveOut = 15,000 × 10^18 (= 1.5e22 base units WETH)
  • amountIn = 26,869 × 10^6 (= 2.6869e10 base units USDC)

计算

amountInWithFee = 2.6869e10 * 997          = 2.6788e13
numerator       = 2.6788e13 * 1.5e22       = 4.0182e35
denominator     = 5e13 * 1000 + 2.6788e13  = 5.02679e16
amountOut       = 4.0182e35 / 5.02679e16   ≈ 7.9936e18   (= ~7.9936 WETH)

不论是用 Solidity uint256、TS BigInt、Python int 来算,最后那个除法的整数部分都完全相同。 这正是为什么 test_Arb_Succeeds_AfterVictimSkewsUniV2 能写 assertEq(profit, 25164517) —— wei-exact。

EVM 层发生的事

─── swap 入口:router.swapExactTokensForTokens(...)─────────────
1. router 把 26,869 USDC 从你账户 transferFrom 走(09 §6 同款流程)
2. router 算 amountOut(用上面公式)
3. router CALL pair.swap(0, amountOut, recipient, "")
─── pair.swap 内部(V2 真正干活的地方)─────────────────────
4. pair: 把 amountOut WETH 转给 recipient
5. pair: 调用 token0/token1 的 balanceOf(self) 检查
   _balance0 * _balance1 >= reserve0 * reserve1 * (1000/997)^N
   ← 这是"恒定乘积加 fee"的链上不变量检查(K-check)
6. 通过 ⇒ SSTORE 新 reserves(这是 swap 单次 ~80k gas 的 SSTORE 来源)
7. emit Swap(sender, amount0In, amount1In, amount0Out, amount1Out, recipient)
─── pair.swap 返回 → router 收到 amountOut → router 转给真正的 recipient ─

→ 这就是 Arbitrageur.t.sol 看到 487,252 gas 的来源之一(07 §8):两次 swap × ~80k SSTORE + flashLoan 周边逻辑。

套利往返:reserves 的对称性

我们的 Arbitrageur 做"借 USDC → Uni V2 换 WETH → Sushi 换回 USDC → 还 USDC + premium":

  • 第一笔(Uni V2 USDC→WETH):reserveIn USDC 增加、reserveOut WETH 减少 → Uni V2 上的 USDC 变便宜(用更少 WETH 能买到一样多 USDC)
  • 第二笔(Sushi WETH→USDC):reserveIn WETH 增加、reserveOut USDC 减少 → Sushi 的 USDC 变贵
  • 两次交易把两个 pool 的价差缩小——这就是套利的"自我消除"特性

→ 如果 Uni V2 的 USDC/WETH 和 Sushi 的相差 30 bps(万分之三十),套利者扫一波后差距落到 ~5 bps, 直到下一笔大单破坏平衡。这就是 Phase 1/4 看到的"残差 ≈ 0"的真实机制。


7. 常见误区(逐条拍死)

误区 实情
"AMM 报价靠预言机喂" 完全内生。价格 = reserves 比的衍生量;外部价靠套利倒推
"x·y=k 真的恒定" swap 后 k 略增(fee 留 pool 内)。真正的不变量是 K-check 公式
"0.3% fee 给协议" V2 全给 LP,间接抬高 LP token 背后 reserves。V3 可选打开 protocol fee,默认仍归 LP
"swap 是公允互换" 必有滑点。大单 effective ≫ spot 的偏离
"大单先成交后摆 pool" 错。一笔 swap 内 amountOut 由 swap 前 reserves 直接算出。没有"分批撮合"概念
"Uniswap V2 是中心化的" V2 是开源 immutable 合约,无人能停(08 §4.2 部署后即冻结)。前端 ui 可被审查,合约不可
"Sushi 跟 Uniswap V2 完全不同" Sushi V2 池是 V2 fork code 1:1(factory 改了名)。差异 = 不同 reserves + 不同流动性深度
"pool 价格能被无限砸偏" 砸越多滑点越大;同时套利者会扫;长期回归外部市场价是统计性事实
"我能挂限价单到 V2" V2 不存在订单簿。limit-order DEX(1inch、Hashflow、CoW)是另一套机制
"V3 比 V2 高效就该用 V3" V3 公式段函数 + tick 量化,wei-exact 推导难。教学场景 V2 公式简单、足够说明 AMM 全部精髓
"AMM 无 impermanent loss" LP 持币 vs 单纯 hold 的回报差就是 impermanent loss;价格偏离越多损失越大(这是 LP 的真实风险)
"pool 地址要查链上注册表" V2 pair 地址确定性计算keccak256(0xff, factory, salt=keccak256(token0,token1), initcodehash);不用查表

8. 与本项目实践的对应

我们 Arbitrageur / MultiPoolArbitrageur 的核心算法

  • getAmountOut(amountIn, reserveIn, reserveOut) 在 Solidity 与 TS 双端实现,公式完全一致(§4.2); 这是 12/12 wei-exact 测试群(07 §8 / 08 §8 / 09 §8)成立的根。
  • 闭式最优 amountIn:给定两个 pool A、B 的 reserves,反向求"使两端价格相等"的 amountIn, 用纯整数 sqrt + 几个乘除——也是 base units 整数,所以最优值在 TS / Solidity 一致。 数学上:amountIn* = (sqrt(R_a_in * R_b_in * R_a_out * R_b_out) − R_a_in * R_b_in) / (R_a_in + R_b_in)(近似); 代码用了带 fee 的精确变体。
  • Phase 4 跨 V2-V2 套利死的真因:套利搜寻者已经把每次偏离都扫了 → 残差 ≈ 0; 这不是机会不存在,是机会的捕获权已被基础设施配齐的人占据(docs 2026-05-19 自我更正 接此)。

pair address 确定性(接 06 §4.5 + 08 §6.2)

  • 给定 token0、token1,UniV2 pair = getPair(token0,token1) 一查即得; 甚至可以链下纯算keccak256(0xff ++ factory ++ keccak256(token0,token1) ++ initcodeHash)[-20:] → 我们 lib.tspairFor() 工具就是这条公式的直接实现,离线即可算出 pair 地址

getReserves() STATICCALL(08 §4.3)

  • IUniswapV2Pair(pair).getReserves() 是 view 函数 → STATICCALL → 只读 → 不能改 reserves。
  • 我们 ComparePools.s.sol 用它纯只读读两个 pool 当前 reserves → 算 spot price → 判断套利方向。

0.3% fee 在 Arbitrageur 里的算法体现

  • amountInWithFee = amountIn * 997 这一行(V2 Periphery 的 UniswapV2Library 代码原样)
  • 来回两次 swap = 两次 0.3% 削皮 ≈ 0.6% 总磨损 → 套利前 spot 价差必须 > 0.6% + Aave 5bps + gas/收益 才有正利润 → 这是 phase4 反复看到"看似 30 bps 价差实际不可执行"的原因

simple-arbitrage 的 BundleExecutor 跟我们 Arbitrageur 是不同的优化点

  • 我们:闪电贷融资 + 单 tx 内两次 swap
  • simple-arbitrage:自有 ETH 本金,省去 5bps premium,跑得更便宜但有本金占用
  • 两条路径都对,看你有没有本金 + 选择信任谁

9. 自检问题(答得出才算过)

  1. V2 的"恒定乘积"真的恒定吗?真正的链上不变量是哪条不等式?为什么这么设计?
  2. 完整写出 getAmountOut(amountIn, reserveIn, reserveOut) 的整数公式。为什么用 997 / 1000?
  3. spot price 和 effective price 的差异叫什么?同样 1 ETH 大小的 trade 在深池和浅池上的差距能有多大?
  4. 用户的"slippage tolerance"参数链上具体起什么作用?哪一行 require 在管它?
  5. LP 的真实风险是什么?impermanent loss 概念是否准确?
  6. 为什么 V2 pair 地址可以纯链下计算?给出公式。
  7. Uniswap V2 和 Sushi 在合约层面差什么?为什么二者价差能持续存在但又会被套利消解?
  8. 我们 getAmountOut 在 TS / Solidity 双端结果完全一致到 wei,依赖的关键性质是什么?哪一类计算会破坏这个性质(举一例)?

── Q&A / 更正记录区(按日期追加)──

规范见 README.md §0。质疑/反馈/纠错按日期累积,不抹错误轨迹。

  • (2026-05-21 建立。暂无 Q&A。)

  • (2026-07-27) 问:详细讲一下交易双方的滑点细节,以及它和 gas、竞价的关系。

    答(补充滑点的精确成因拆分与三明治攻击串联机制,未更正原文;完整版含 calldata 字节级示例见 ../total/Q&A/2026-07-27-链上账本手记.html 第六节 F/G):

    精确定义:滑点 = 提交交易时看到的预期成交价格,与交易真正落地时的实际成交价格之间的差。 两种完全不同的成因,容易被混为一谈:

    1. 机制性滑点(跟有没有人抢跑无关):AMM 按 x·y=k 曲线定价,交易本身相对池子深度越大, 把价格顺着曲线推得越远,成交均价必然比"下单前一刻的瞬时价格"差——这是纯数学结果,池深不变、 没有第三者插手也会发生。
    2. 时间差滑点(跟竞价排序直接相关):从拿到报价到交易真正被打包执行,中间隔着排队等待的时间; 如果别的交易先成交、把价格挪走了,实际吃到的价格就比报价时更差——这部分滑点大小取决于自己的 priority fee 出得够不够高、排得够不够靠前(见 14-mempool与PBS.md)。

    amountOutMin(或买单的 amountInMax 就是"滑点容忍度"在调用参数里的具体落地:能接受的最差 成交结果,写死在 calldata 里;链上执行到检查"实际输出是否 ≥ amountOutMin",不满足就整笔 revert (revert 之前已消耗的 gas 依然要付)。

    三明治攻击 = 竞价排序权 + 滑点容忍空间拧在一起的具体机制:攻击者用更高 priority fee 把买单排在 受害交易前面成交、推高价格,吃掉受害者 amountOutMin 的容忍空间,随后立刻反向卖出获利。 滑点容忍度设得越宽,留给这种攻击的可榨取空间就越大——"滑点容忍度不是越大越保险,反而越大越 危险"这个反直觉结论的根源就在这里。

  • (2026-07-27 · 续) 问:更细致地讲滑点为什么和买卖数量、"公认"价格、竞争机制有关,以及买卖价格 怎么反过来影响链上币的价值。

    答(在上一条基础上,把"滑点为什么发生"往前推到"价格从哪来"这个更根本的问题,未更正原文; 完整版含实测表格见 ../total/Q&A/2026-07-27-链上账本手记.html 第六节 F 的四段新增内容):

    价格不是被写进去的,是被"算"出来的公认值:池子没有任何地方存"当前价格"字段,只存两种代币各自的 数量(reserves),价格 = 两个数量的比例现算出来的。没有人"公告"这个价格——它是全网每一笔历史成交 共同留下的残留物:谁愿意用多少 A 换多少 B,成交完这两个数量就永久变一点,下一个人看到的"当前价格"就是 所有前人交易行为叠加后的比例快照。这就是"公认"在链上的准确含义:不是投票表决,是用真金白银的成交 历史"投票"出来的,且这份记录本身写在链上账本里,人人可读可验证,不需要权威机构宣布。

    数量怎么决定价格冲击(公式 + 实测表)x·y=k 下,投入 Δx 换出 Δy = y·Δx/(x+Δx),有效成交价 = (x+Δx)/y,现货价 = x/y,价格溢价 = Δx/x——交易数量占池子已有数量的比例越大,溢价越大, 接近线性关系(小额时)。实测(1,000,000 USDC / 500 WETH,现货价 2000):投入占比 0.1% 少拿 0.1%; 占比 10% 少拿 9.1%;占比 50% 少拿 33.3%——池子越浅、交易量相对越大,"少拿"比例越夸张,纯几何/代数 关系,跟有没有人操纵无关。"币的数量"(池深 vs 交易量)直接决定了买卖动作对价格的冲击力度

    竞争机制怎么把"公认价格"拉回外部市场:池子价格和外部价格不一致时出现套利机会,套利者的买卖动作 本身就是上面公式里的"Δx 投入",把数量比例推回"正确"方向直到两边对齐——池子价格能跟上外部市场, 靠的是套利这个财务动机自动驱动的,不是任何人维护。多个套利者同时盯着同一价差,靠 priority fee 竞速, 谁的交易先打包谁吃到差价,池子价格因此常在一个区块内被拉回去。

    买卖为什么会反过来影响链上"币的价值"本身:在没有外部预言机时,一个代币在链上的"价值"不是被写进 链上的既定事实,而是完全由买卖双方的交易数量史构造出来的可读数值——每一次买卖不仅让发起人自己承受 滑点,还永久性地重写下一个人将看到的"公认价格"。"价格发现"这个词用在 AMM 上格外贴切:价格不是 被发现了一个客观外部真相,是被每一笔实际发生的买卖持续构造出来的。