基于下面的例子, 我们会画一个完整的「Uniswap V2 交易曲线」示意图:

初始池:1000 A + 2000 B
用户:用 100 A 兑换 B
手续费:0.3%
实际输出:≈ 181.64 B
恒定积 ( k = 2{,}000{,}000 )


🧮 一、恒定积模型原理

Uniswap V2 的价格机制遵循:

[
x \times y = k
]

在坐标平面上:

  • x 轴 = Token A 储备
  • y 轴 = Token B 储备
  • 曲线 ( y = \frac{k}{x} ) 是一条双曲线(即恒定积曲线)

📈 二、交易前后的储备与价格变化示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   y轴: Token B 储备

2000 |● 初始点 P₀ (x=1000, y=2000)
| \
| \
| \
| \
1818 |------● 新平衡点 P₁ (x=1099.7, y≈1818)
| \
| \
| \
| \
+----------------------------------------→ x轴: Token A 储备
1000 1099.7

🟢 点 P₀:初始状态

  • ( reserveA = 1000 )
  • ( reserveB = 2000 )
  • ( P_A = reserveB / reserveA = 2.0 )

🔵 点 P₁:交易后状态

  • ( reserveA’ = 1099.7 )
  • ( reserveB’ = 1818.36 )
  • ( P_A’ = 1818.36 / 1099.7 = 1.653 )
  • 实际成交均价约 1.8164(中间价格)

📉 三、滑点(Slippage)展示

项目 含义 数值
初始价格 ( 1A = 2B ) 2.000
交易后瞬时价格 ( 1A = 1.653B ) 1.653
实际成交均价 平均约 1.816B 1.816
滑点比例 ( (2 - 1.816) / 2 ) 9.2%

🧠 解释:
用户预期 1A=2B,但由于交易影响价格曲线,使池内的 A 增多、B 减少,价格被推低,
因此最终实际获得的平均兑换比例变差。


🔄 四、k 恒定线变化对比

状态 reserveA reserveB A×B = k 备注
交易前 1000 2000 2,000,000 初始状态
交易后(理论恒定) 1099.7 1818.36 ≈2,000,000 恒定积保持
实际变化(含手续费) 部分 k 增加 → 手续费流入池中,k 略增大

手续费 0.3% 进入池子,使得流动性提供者 (LP) 的 k 略上升,
相当于 LP 赚取了一点手续费收入。


🪙 五、交易动态路径说明

1
2
3
4
5
6
7
8
交易过程(从 P₀ 到 P₁):
用户输入 100 A ->
扣除手续费 0.3 A ->
实际进入池中 99.7 A ->
曲线向右移动 ->
池中 B 减少至 1818 ->
用户获得 181.64 B ->
曲线保持 k 恒定

在曲线上移动的轨迹表示价格逐渐变化的路径,
价格不是跳变,而是沿曲线连续变化的。


🧭 六、图像总结(文字版思维导图)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
恒定积公式: x * y = k

├─ 初始点 P₀: (1000, 2000)
│ └─ 初始价格 = 2

├─ 用户输入 100 A
│ ├─ 扣手续费 -> 实际输入 99.7 A
│ ├─ 新A储备 = 1099.7
│ ├─ 计算新B储备 = k / x' = 1818.36
│ └─ 输出B = 2000 - 1818.36 = 181.64

├─ 新点 P₁: (1099.7, 1818.36)
│ └─ 新价格 = 1.653

└─ 价格曲线滑动:
曲线右移 -> 滑点上升 -> 实际价格下降

🧩 七、关键理解总结

概念 解释
k 恒定曲线 确保交易自动计算价格,不需要订单簿
滑点 交易越大,占池子越多,价格偏离越明显
手续费 每笔 0.3% 计入流动性池,增强 LP 收益
价格动态性 交易沿曲线移动,而非线性固定价格
k 的微增 因手续费注入,流动性略微提升

好——下面我把之前的恒积曲线 (y=\dfrac{k}{x}) 用两种形式画出来:

  • 一份 ASCII 文字图(便于在终端或纯文本环境查看),
  • 一份 SVG 矢量图源码(可直接复制到 .svg 查看器 / 浏览器中,或嵌入 HTML,用于精确展示)。

我会在图上明确标出:初始点 (P_0(1000,2000))、交易后点 (P_1(1099.7,;1818.6778))、滑点范围、以及曲线方向箭头。
下面先给出关键数值(逐位计算,便于复核):

  • 初始储备:reserveA = 1000reserveB = 2000
  • 恒定积:(k = 1000 \times 2000 = 2{,}000{,}000)。
  • 用户输入:amountIn = 100(Token A)。
  • 手续费 0.3%,因此 amountInWithFee = 100 × 0.997 = 99.7
  • 交易后 A 储备:(x’ = 1000 + 99.7 = 1099.7)。
  • 交易后 B 储备:(y’ = k / x’ = 2{,}000{,}000 / 1099.7 \approx 1818.67782122397)。
  • 用户获得 B:amountOut = 2000 - y' ≈ 181.32217877603
  • 说明:交易使价格由 1A=2B 降到瞬时价格约 (1A \approx 1.653B)(池内瞬时比值),而用户成交的平均价格约 (1.816B)(见前述推导)。

一、ASCII 版本(文字示意图)

说明:ASCII 用字符网格近似表示曲线位置、P₀、P₁ 与滑点区域(非精确图形,但能直观看出移动方向与相对位置)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
                 Token B 储备 (y)
2200 +------------------------------------------------+
| |
| *P0 (1000, 2000) |
2000 +-----------------*-------------------------------+
| /
| /
| /
| /
1850 +----------*P1 (1099.7,1818.68)------------------+ ← 交易后点,y' ≈ 1818.68
| / |
| / |
| / |
| / |
1700 +-------/----------------------------------------+
| / |
| / |
| / |
| / |
1500 +--/---------------------------------------------+
| / |
|/ |
+------------------------------------------------+--> Token A 储备 (x)
800 900 1000 1099.7 1200 1300 1400

说明:
- 曲线为 y = k / x(向右下方渐近)。初始点 P0 在 (1000,2000)。
- 用户以 A 增加 99.7(含手续费后),曲线从 P0 沿恒积曲线向右移动到 P1 (1099.7,1818.68)。
- 滑点体现在 P0 的切线价格(约 2.0)与 P1 的瞬时价格(约 1.653)之间的差距;用户实际成交均价介于二者(≈1.816)。
- 曲线方向由上左指向下右(箭头方向表明当 x 增大时 y 减小)。

二、SVG 版本(可复制为 .svg 文件并在浏览器打开)

下面是一个完整的 SVG 文本(宽 900px × 高 600px),它绘制了 (y=k/x) 的曲线(在 x 范围 600…1600),并标注了 P₀、P₁、滑点箭头与说明文字。你可直接把下列内容保存为 uniswap_curve.svg 然后在浏览器打开,或在 README/网页中嵌入 <svg>...</svg>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<?xml version="1.0" encoding="UTF-8"?>
<svg width="900" height="600" viewBox="0 0 900 600" xmlns="http://www.w3.org/2000/svg">

<!-- Background -->
<rect x="0" y="0" width="900" height="600" fill="#ffffff" />

<!-- Axes -->
<g id="axes" stroke="#444" stroke-width="1" fill="none">
<!-- X axis -->
<line x1="80" y1="520" x2="820" y2="520" />
<!-- Y axis -->
<line x1="80" y1="520" x2="80" y2="80" />
</g>

<!-- Axis labels -->
<text x="820" y="540" font-family="Arial" font-size="12" fill="#222">Token A 储备 (x)</text>
<text x="20" y="80" font-family="Arial" font-size="12" fill="#222" transform="rotate(-90 20,80)">Token B 储备 (y)</text>

<!-- Tick marks and labels on X -->
<g font-family="Arial" font-size="11" fill="#333">
<!-- We'll map x in [600,1600] to svg x in [100,760] -->
<!-- ticks at 700,800,900,1000,1100,1200,1300,1400,1500 -->
<!-- compute positions manually approximate -->
<line x1="100" y1="520" x2="100" y2="525" stroke="#666"/>
<text x="92" y="540">700</text>

<line x1="160" y1="520" x2="160" y2="525" stroke="#666"/>
<text x="152" y="540">800</text>

<line x1="220" y1="520" x2="220" y2="525" stroke="#666"/>
<text x="212" y="540">900</text>

<line x1="280" y1="520" x2="280" y2="525" stroke="#666"/>
<text x="272" y="540">1000</text>

<line x1="340" y1="520" x2="340" y2="525" stroke="#666"/>
<text x="332" y="540">1100</text>

<line x1="400" y1="520" x2="400" y2="525" stroke="#666"/>
<text x="392" y="540">1200</text>

<line x1="460" y1="520" x2="460" y2="525" stroke="#666"/>
<text x="452" y="540">1300</text>

<line x1="520" y1="520" x2="520" y2="525" stroke="#666"/>
<text x="512" y="540">1400</text>

<line x1="580" y1="520" x2="580" y2="525" stroke="#666"/>
<text x="572" y="540">1500</text>
</g>

<!-- Tick marks and labels on Y -->
<g font-family="Arial" font-size="11" fill="#333">
<!-- map y in [1400,2200] to svg y in [420,100] -->
<line x1="80" y1="420" x2="75" y2="420" stroke="#666"/>
<text x="40" y="424">1400</text>

<line x1="80" y1="360" x2="75" y2="360" stroke="#666"/>
<text x="40" y="364">1600</text>

<line x1="80" y1="300" x2="75" y2="300" stroke="#666"/>
<text x="40" y="304">1800</text>

<line x1="80" y1="240" x2="75" y2="240" stroke="#666"/>
<text x="40" y="244">2000</text>

<line x1="80" y1="180" x2="75" y2="180" stroke="#666"/>
<text x="40" y="184">2200</text>
</g>

<!-- Draw y = k / x curve (sampled points) -->
<!-- Map function: x in [600,1600] -> sx in [100,760]; y in [1400,2200] -> sy in [420,100] -->
<!-- We'll sample x values and compute y = 2,000,000 / x -->
<path d="
M 100 ( ${ /* placeholder - will be replaced by computed points below */ '' } )
" stroke="#ff6a00" stroke-width="2" fill="none" id="curve"/>

<!-- Instead of trying to construct path via string interpolation here,
we'll include a precomputed path below for the range x=600..1600 sampled. -->

<path d="
M 100,351.666
C 130,342 160,334 190,327
C 220,320 250,314 280,309
C 310,304 340,300 370,296
C 400,292 430,289 460,286
C 490,283 520,281 550,279
C 580,277 610,275 640,274
C 670,272 700,271 730,270
C 760,269
" stroke="#ff6a00" stroke-width="3" fill="none" opacity="0.95"/>

<!-- Note: 上面曲线路径是手工近似采样,以下再准确绘制并标注关键点 P0 和 P1 -->

<!-- Precise mapping helper lines & compute positions (we will place points by computed coords) -->
<!-- Mapping functions used to compute below:
sx(x) = 100 + (x - 600) * (660 / 1000) (maps 600..1600 to 100..760)
sy(y) = 420 - (y - 1400) * (320 / 800) (maps 1400..2200 to 420..100)
-->

<!-- Compute P0: x=1000, y=2000 -->
<!-- sx(1000) = 100 + (1000-600)*(660/1000) = 100 + 400*0.66 = 100 + 264 = 364 -->
<!-- sy(2000) = 420 - (2000-1400)*(320/800) = 420 - 600*(0.4) = 420 - 240 = 180 -->

<!-- Compute P1: x=1099.7, y≈1818.67782122397 -->
<!-- sx(1099.7) = 100 + (1099.7-600)*0.66 ≈ 100 + 499.7*0.66 ≈ 100 + 329.802 ≈ 429.802 -->
<!-- sy(1818.6778) = 420 - (1818.6778-1400)*0.4 ≈ 420 - 418.6778*0.4 ≈ 420 - 167.47112 ≈ 252.52888 -->

<!-- Mark P0 -->
<circle cx="364" cy="180" r="5" fill="#0055aa" />
<text x="374" y="176" font-family="Arial" font-size="12" fill="#0055aa">P₀ (1000, 2000)</text>

<!-- Mark P1 -->
<circle cx="429.8" cy="252.53" r="5" fill="#d9534f" />
<text x="440" y="248" font-family="Arial" font-size="12" fill="#d9534f">P₁ (1099.7, 1818.68)</text>

<!-- Draw an arrow along the curve from P0 to P1 to show movement -->
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="6" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#333" />
</marker>
</defs>

<!-- A short curved arrow between P0 and P1 -->
<path d="M 370 190 Q 400 220 426 250" stroke="#333" stroke-width="1.4" fill="none" marker-end="url(#arrow)" />

<!-- Draw small vertical bracket showing sliding from initial price to new price (visual of slippage) -->
<!-- At x ~ 364 (P0 x), show vertical from y=180 to y≈? corresponding P1 price line for that x (visual) -->
<line x1="364" y1="180" x2="364" y2="252.53" stroke="#999" stroke-dasharray="4 4"/>
<text x="290" y="220" font-family="Arial" font-size="12" fill="#666">滑点: 价格从 2.00 → 1.653 (瞬时)</text>

<!-- Annotate amountOut -->
<text x="520" y="300" font-family="Arial" font-size="12" fill="#222">
用户输入 100 A → 实得 ≈ 181.322 B (amountOut)
</text>

<!-- Legend -->
<rect x="600" y="60" width="270" height="110" fill="#f8f8f8" stroke="#ddd"/>
<text x="612" y="80" font-family="Arial" font-size="13" fill="#333">图例 / 说明</text>
<circle cx="620" cy="98" r="5" fill="#0055aa" />
<text x="634" y="102" font-family="Arial" font-size="12" fill="#333">P₀: 交易前 (1000,2000)</text>
<circle cx="620" cy="120" r="5" fill="#d9534f" />
<text x="634" y="124" font-family="Arial" font-size="12" fill="#333">P₁: 交易后 (1099.7,1818.68)</text>
<line x1="610" y1="140" x2="636" y2="140" stroke="#333" stroke-width="3" />
<text x="640" y="144" font-family="Arial" font-size="12" fill="#333">曲线: y = k / x (k = 2,000,000)</text>

<!-- Footer text -->
<text x="80" y="565" font-family="Arial" font-size="11" fill="#333">
注: 曲线为 y = k/x 的近似示意;坐标映射为 x∈[600,1600] → svg_x∈[100,760], y∈[1400,2200] → svg_y∈[420,100]。
</text>
</svg>

说明(关于 SVG 中的映射与精度):

  • 为了把数学坐标映射到 SVG 像素,我对 x,y 做了线性缩放(在注释处给出公式);
  • 曲线路径做了近似采样与手工平滑绘制以保证视觉美观;关键点 P₀、P₁ 的像素位置按精确数值计算并标注(你可按需用更高采样密度替换曲线路径以提高精确度)。

✅ 这里是完整的结果,包括两种可视化形式:


1️⃣ 数学真实曲线 (SVG/图形版)

上面的图展示了 Uniswap v2 的恒定乘积曲线 ( y = \frac{k}{x} ),其中:

  • P₀ (绿色点):交易前储备状态

    • ReserveA = 1000
    • ReserveB = 2000
    • ( P₀ = \frac{2000}{1000} = 2 )
  • P₁ (红色点):交易后储备状态

    • ReserveA = 1100
    • ReserveB ≈ 1818.18
    • ( P₁ = \frac{1818.18}{1100} ≈ 1.653 )
  • 蓝色箭头表示交易方向(用户加入 A → 储备A上升 → 储备B下降)

  • 曲线向右下弯曲说明:随着 A 储备增加,B 储备减少,价格曲线非线性,滑点出现。


2️⃣ ASCII 文字版示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  Token B Reserve (y)
|
2200 | *
| *
| *
| *
2000 | * ← P₀ (Before Trade)
| *
| *
|*
1800 |* ← P₁ (After Trade)
|
|_________________________________
Token A Reserve (x)

Trade Direction
(User adds A → Reserve A↑, Reserve B↓)

解释总结:

项目 说明
k = x × y 恒定乘积不变,曲线满足 y = k/x
滑点产生 因为曲线非线性,加入 100 A 不会线性换出 200 B,而是少于 200
价格变化 交易前 1A = 2B,交易后 1A ≈ 1.653B
k 恒定线 图中曲线即为 “k 恒定线”,任何交易都在曲线上移动,但不离开它
视觉方向 从左上到右下,表示 “买入 A 会导致 B 价格上涨(A 贬值)”

问题: 如果 reserveA = 1000, reserveB = 2000 所以初始价格 P_A = reseverB / reserveA = 2000/1000 = 2, 即 1A = 2B, 因此如果用户想用 100A 换取 B, 是否应该能换 200B ? 再扣去手续费0.3% 相当于 200 * 99.7%?

回答: “用 100A 换 200B(再扣手续费)”这个计算是线性思维(即假设价格不变),而在 AMM(自动做市商)中,价格会随着交易发生而动态变化,这就是所谓的 滑点(slippage)

详细推导如下👇:


🔹前提条件

池中初始储备:

  • reserveA = 1000
  • reserveB = 2000
  • 因此初始价格:
    $P_A = \frac{reserveB}{reserveA} = \frac{2000}{1000} = 2$,
    1 A = 2 B

手续费 = 0.3%
手续费后有效输入比例 = 0.997


🔹Uniswap V2 的核心恒等式:

$$
x \times y = k
$$

其中:

  • $x$ 是 token A 的储备量
  • $y$ 是 token B 的储备量
  • $k$ 是常数(交易前后必须保持不变)

🔹用户用 100 A 换 B 的详细过程

1️⃣ 用户输入的 A:

$$
\text{amountIn} = 100
$$

2️⃣ 扣除手续费:

$$
\text{amountInWithFee} = 100 \times 0.997 = 99.7
$$

3️⃣ 新的 A 储备:

$$
x’ = reserveA + amountInWithFee = 1000 + 99.7 = 1099.7
$$

4️⃣ 根据恒定积公式 $x \times y = k$,
交易前 $k = 1000 \times 2000 = 2,000,000$

所以:

$$
y’ = \frac{k}{x’} = \frac{2,000,000}{1099.7} \approx 1818.36
$$

5️⃣ 用户能拿到的 B 数量:

$$
\text{amountOut} = reserveB - y’ = 2000 - 1818.36 = 181.64
$$


✅ 结论

  • 用户输入:100 A
  • 实际获得:≈ 181.64 B
  • 实际价格:$\frac{181.64}{100} = 1.8164$
    (低于初始价格 2,原因是滑点)

🔹对比问题中的计算

问题中线性比例算得是:

$$
100A \times 2 = 200B
$$

再扣 0.3% 得 $200 \times 0.997 = 199.4B$。

➡️ 这是静态价格假设下的计算,但 AMM 实际上价格随交易动态变化
由于用户一次性投入 100 A(池子总量的 10%),对价格影响很大,
所以用户只能拿到 181.64 B,而不是 199.4 B。


🔹总结

概念 含义
k 恒定积 保证池子总流动性恒定:$x \times y = k$
滑点 大额交易导致价格变化,得到的代币变少
手续费 每笔交易收取 0.3%,分给流动性提供者
价格动态性 每次 swap 都会改变 reserve,从而改变价格
实际输出公式 $\text{amountOut} = \frac{amountInWithFee \times reserveOut}{reserveIn \times 1000 + amountInWithFee}$

深入例子与解析 —— Uniswap V2(恒积 AMM)流动性池各方面细节

下面我把 Uniswap V2(或 PancakeSwap V2 类似实现)的原理 + 合约调用 + 数学计算 + 常见问题用若干详细、可复现的例子彻底讲清楚(所有算术按步骤逐位计算,避免四则运算偷步造成错误)。


1. 恒积公式与价格基础(x × y = k)

  • 池子保存两个代币的储备:reserveA(Token A)与 reserveB(Token B)。
  • 恒积不变:reserveA × reserveB = k(在没有手续费前,任何交换都应保持 k 不变)。
  • 瞬时价格(A 的价格以 B 计)近似为 price_A_in_B = reserveB / reserveA(注意是池中瞬时比值,不是离散市场深度)。

2. 示例:创建池子(第一次添加流动性)

假设项目方首次把 1000 个 TokenA(记作 A)和 2000 个 TokenB(记作 B)放入池子。

  • 初始 reserves:reserveA = 1000reserveB = 2000
  • 初始价格 P_A = reserveB / reserveA = 2000 / 1000 = 2(即 1 A = 2 B)。

LP 代币铸造(首次)
Uniswap V2 首次提供流动性时,mint 的 LP 数量为:

$$
LP\text{_initial} = \sqrt{amountA \times amountB} - \text{MINIMUM_LIQUIDITY}
$$

其中通常 MINIMUM_LIQUIDITY = 1000(被锁定燃烧掉以避免除以 0 的问题)。

计算:

  1. 计算乘积 amountA * amountB = 1000 * 2000 = 2,000,000

  2. 平方根 sqrt(2,000,000)。逐位算:

    • sqrt(2,000,000) ≈ 1414.2135623730950488016887242097...(常见值)
  3. 初始 LP minted ≈ 1414.213562373095 - 1000 = 414.213562373095(约 414.2136 LP 代币 发给最初提供者)。

(注:生产实现取整/小数位通常由合约逻辑决定)


3. 例子:执行一次 Swap(含手续费 0.3%)

池当前:reserveA = 1000, reserveB = 2000。用户想用 100 A 换取 B(swap A -> B)。Uniswap V2 的手续费是 0.30%,实际流入池的 amountInWithFee = amountIn * 997 / 1000(997 = 1000 - 3)。

步骤(逐位算)

  1. amountIn = 100(A)。

  2. 计算含费后的数值:

    • amountInWithFee = 100 * 997 / 1000
    • 先算 100 * 997 = 99,700
    • 然后 99,700 / 1000 = 99.7
      amountInWithFee = 99.7(A 的有效输入)
  3. 计算 amountOut(B)用公式:

    $$
    amountOut = \frac{amountInWithFee \times reserveB}{reserveA + amountInWithFee}
    $$

    把数值代入:

    • 分子:99.7 × 2000 = 199,400
    • 分母:1000 + 99.7 = 1,099.7
    • 除法:199,400 / 1,099.7 ≈ 181.182897...
  4. 所以用户大约能收到 181.182897 B(合约会四舍五入到可精度的整数或 token 的最小单位)。

更新后池子储备

  • reserveA = 1000 + 100 = 1100(注意实际池里增加的是 amountIn 全额 100,而计算中用 amountInWithFee 来保持恒积关系)
  • reserveB = 2000 - amountOut ≈ 2000 - 181.182897 = 1818.817103
  • 检查恒积近似:1100 * 1818.817103 ≈ 2,000,698.813(和原来 2,000,000 比较差异来自手续费留在池中,k 会增加一点)

4. LP 的增删(添加流动性到已有池)

已存在池:reserveA = 1000, reserveB = 2000, totalSupplyLP = S(例如最初 S=1414.2136)。新提供者想加 xA = 100 A 与 xB = ?

  • 必须按当前比例添加以避免改变价格:

    $$
    xB = xA \times \frac{reserveB}{reserveA}
    $$

    例如:xB = 100 × 2000 / 1000 = 200

  • 若提供者按这个比例加入,获得的 LP token 数量为:

    $$
    liquidity = \frac{xA \times totalSupplyLP}{reserveA} = \frac{xB \times totalSupplyLP}{reserveB}
    $$

    (两者应相等)

示例计算(若 totalSupplyLP = 2000 假设):

  • liquidity = 100 * 2000 / 1000 = 200(也等于 200 * 2000 / 2000 = 200

若未按比例加入(只提供一种或比例不同),Router 会计算最小值并退回多余资产(或按最小比率扣除)。


5. 移除流动性(退出)

  • 移除 L 个 LP token,退出比例为:L / totalSupplyLP

  • 提取出的代币数量:

    $$
    amountA = \frac{reserveA \times L}{totalSupplyLP}, \quad amountB = \frac{reserveB \times L}{totalSupplyLP}
    $$

  • 例:若 L = 200, reserveA = 1100, reserveB = 1818.817103, totalSupplyLP = 2000

    • amountA = 1100 * 200 / 2000 = 110
    • amountB = 1818.817103 * 200 / 2000 = 181.8817103

6. 无常损失(Impermanent Loss)详细数值示例

这是理解 LP 风险的关键,实际算例最能说明问题。

初始

  • 用户提供流动性:reserveA = 10 A, reserveB = 100 B(于是初始价格 P_A = 100/10 = 10 B,即 1 A = 10 B)。
  • k = 10 * 100 = 1000.

价格变动:假设外部市场使得 A 的价格从 10 B 变为 20 B(价格翻倍)。交易者与套利者会对池做套利使池内价格等于市场价(即 reserveB / reserveA = 20),在保持 k 不变下:

解方程:

  • reserveB = 20 * reserveA
  • 恒积 reserveA * reserveB = reserveA * (20 * reserveA) = 20 * reserveA^2 = 1000
  • 所以 reserveA^2 = 1000 / 20 = 50
  • reserveA = sqrt(50) ≈ 7.0710678118654752440084436210485(保留精度)
  • reserveB = 20 * reserveA ≈ 20 * 7.071067811865475 = 141.4213562373095

LP(若持有原来池的全部)在此时拥有的资产

  • amountA = reserveA ≈ 7.071067811865475
  • amountB = reserveB ≈ 141.4213562373095

以当前市场价(1 A = 20 B)计价,总价值为

  • value_in_B = amountA * 20 + amountB = 7.071067811865475 * 20 + 141.4213562373095
  • 先算 7.071067811865475 * 20 = 141.4213562373095
  • 再加上 141.4213562373095 得到 282.842712474619(B 单位)

如果不作为 LP,而是原样持有(HODL)

  • 最开始持有的是 10 A + 100 B,价格翻倍后价值:

    • A 部分:10 * 20 = 200 (B)
    • B 部分:100 (B)
    • 总价值 = 200 + 100 = 300 (B)

无常损失 = 相对差距

  • 绝对差距 = 300 - 282.842712474619 = 17.157287525381
  • 相对损失 = 17.157287525381 / 300 ≈ 0.057191 = 5.7191%

→ 即使池中包含手续费,LP 的收益需与手续费收入合并比较才能判断最终是否优于直接 HODL。


7. 费用与对 LP 收益的影响

  • 每笔交易收取 0.30% 费用,直接加入池中(增加 k)。这些费用会累积到 reserveA/reserveB 中,从而逐步补偿 LP 的无常损失并给 LP 带来收益。
  • 当交易量高且手续费收入大时,LP 收益可能覆盖甚至超越无常损失。

8. 路由与多段路径(swap path)

Router 提供的常见函数:

  • swapExactTokensForTokens(amountIn, amountOutMin, path[], to, deadline)
  • swapTokensForExactTokens(amountOut, amountInMax, path[], to, deadline)
  • swapExactETHForTokensswapExactTokensForETH 等(处理原生币时的包装)

路径示例:若没有直接 A/B 池,但存在 A/WETH 与 WETH/B 池,路由 path = [A, WETH, B] 会先 A->WETH 再 WETH->B。Router 会组合多池实现最优兑换(但用户需要关注滑点与累计手续费)。


9. 合约内部常见函数(Uniswap V2 的关键点)

  • Factory.createPair(tokenA, tokenB):若 pair 不存在则创建 pair(返回 pair 合约地址)。
  • Pair.mint(to):在添加流动性时 mint LP token。
  • Pair.burn(to):移除流动性时烧毁 LP 并转回 token。
  • Pair.swap(amount0Out, amount1Out, to, data):执行 swap;注意 swap 要求最后 balance0 * balance1 >= k(含手续费)以防篡改。
  • Pair.sync():外部改变 token 余额后强行更新 reserves(很少手动调用)。
  • Pair.skim(to):把多余 token(误发送)转到 to。

闪兑(Flash swap)swap 可允许借出任意数量(0…reserve),并在同一交易内回传等值资产或支付相应费用 —— 类似 flash loan 的一个形式。开发者与审计需小心。


10. TWAP 与价格 Oracle(Uniswap v2 的实现)

  • Pair 合约维护 price0CumulativeLastprice1CumulativeLast(累积(price * time))以便离链或链上通过差分计算时间加权平均价格(TWAP)。
  • 计算方法概念:在两个时间点 t0t1,TWAP = (priceCumulative(t1) - priceCumulative(t0)) / (t1 - t0)
  • 因为 cumulative 是累加 price×时间,取差除以时间即可得到时间加权平均价,抵抗短期操纵。

11. 前置与防护(实务注意)

  • 滑点容忍(amountOutMin):全部 swap 都应设置最小可接受输出以避免因前置/价格波动导致巨额损失。
  • deadline 参数:避免交易长时间挂起,被复议。
  • 重入、批准浪费、审批最小化:在合约层需注意 approve 的使用与最小权限原则。
  • 前置/MEV风险:交易可能被矿工/验证者重排序或夹带,从而产生更差的执行(前置/插队)。可用保护策略(取消/限制滑点、高 gas 优先)但难以完全避免。
  • 闪电贷攻击面:任何外部脚本可在单一 tx 中影响价格并操控 oracle,需在系统设计中考虑价格源多样化与检查(例如 TWAP 的窗口长度)。

12. 典型调用示例(伪代码)

  • 创建池(如果不存在):

    1
    2
    // 返回 pair 地址
    factory.createPair(tokenA, tokenB);
  • 添加流动性(router):

    1
    2
    3
    4
    5
    6
    router.addLiquidity(
    tokenA, tokenB,
    amountADesired, amountBDesired,
    amountAMin, amountBMin,
    to, deadline
    );
  • 用 ETH/BNB 添加:

    1
    2
    3
    4
    5
    6
    router.addLiquidityETH(
    token, amountTokenDesired,
    amountTokenMin, amountETHMin,
    to, deadline,
    { value: amountETH }
    );
  • 交换(确保先 approve token):

    1
    2
    3
    router.swapExactTokensForTokens(
    amountIn, amountOutMin, path, to, deadline
    );

13. 小结与最佳实践(工程及策略)

  • 初学者用例:在测试网做 addLiquidityswapremoveLiquidity 的完整流程来观察 reserves、LP mint、手续费累计与价格变化。
  • 提供流动性前请评估:预计交易量(fee 收入),代币波动性(IL 风险),以及是否需要锁定 LP Token 提升可信度。
  • 使用 Router 而非直接 Pair:Router 帮你处理路径和多池组合,简化调用。
  • 监控:持续监控池深度、滑点、套利窗口与手续费收入,适时调整策略。
  • 安全:审计自制合约(如 Router/Helper)并尽量减少私钥暴露,避免自动化工具导入大量热私钥。

错过了比特币巴士?别再错过CCBus!通往Web3革命的直达车票


🎯 核心标语:

加密财富巴士已到站——立即上车!

还记得比特币100美元的时候吗?还记得以太坊只是个概念的时候吗?那种眼睁睁看着早期参与者积累世代财富的遗憾感?

错过机遇的时代,今天正式结束。


💎 CCBus的独特优势:

🚌 Web3新手专属:

  • 用通俗语言学习区块链基础
  • 首次购买加密货币的步骤指南
  • 安全可靠的交易实践环境
  • 7×24小时社区支持

🚀 加密专家进阶:

  • 高级DeFi策略和收益 farming
  • 最新Layer2解决方案和扩容技术
  • NFT铸造和交易大师课
  • 机构级分析工具

🌟 为什么选择CCBus:

实时学习:使用真实市场数据实践
动手经验:边学边构建真实dApp
社区驱动:向成功加密前辈学习
未来技能:保持区块链趋势领先
财富积累:应用真正有效的策略


🎯 行动召唤:

"财富列车即将离站。这次,你拥有头等舱车票。"

👉 立即加入CCBus,改变你的加密之旅:
访问CCBus.cc | [开始学习]

不要只是旁观革命——成为革命的一部分。

#CCBus #Web3革命 #加密教育 #财富积累


0%