UniswapV2

源代码网址

Uniswap GitHub 仓库

主要仓库

Uniswap Swap Router 合约

恒定乘积自动做市商

image-20250329150310816

x*y=k

x: token 0的数量

y:token 1 的数量

K: 流动性

如果用户卖出token0 则:

(x+dx)*(y-dy) = k = x * y

例子

1ETH/USDT

10 ETH 20000 usdt

K = 200000

  1. 用户卖出5 eth

    (10+5)* (20000-dy) = 200000

    dy = 6666.6 U

2 假如用户持有 10000 u 买入eth

(10 -dx) * (20000+10000) = 200000

dx = 3.33 eth

3用户卖出100000 eth (意思是我们可以几乎掏空我们想要买的币,但是永远不会掏空)

(10+100000)*(200000-dy) = 200000

dy = 19998 u

spot price :

px = 20000/10 = 2000 u

py = 10/20000 = 0.005 eth

滑点

(执行价格 - 预期价格) /预期价格 *100%

流动性

x*y = k = L * L

image-20250329154310635

Uinswap 中的swap

用户是如何swap的

  1. 用户先和rounter合约交互
  2. rounter合约调用目标pair合约
  3. 选择某一种方法两种方法: swapExactTokensForTokens , swapTokensForExactTokens
  4. 调用transferFrom 给 pair
  5. pair合约调用swap
  6. transfer 返还用户

image-20250427133532223

2假如 DAI ->usdt –> MKR

  1. 用户先和rounter合约交互
  2. rounter合约调用目标pair合约
  3. 选择某一种方法两种方法: swapExactTokensForTokens , swapTokenForExcutTokens
  4. 调用transferFrom , dai转入
  5. (dai/Usdt)这个池子, swap后出来的usdt 给到(usdt/MKR)swap后出来的MKR 通过transfer返还给用户

(如果要换取小众代币,需要我多次跳跃)

image-20250427133556497

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
function swapExactTokensForTokens(
// 代币换代币(确定代币A的数量系统给出可以换取的代币B的数量)
uint256 amountIn, // 输入的代币A的数量
uint256 amountOutMin, // 期望换取最少得代币B数量
address[] calldata path, // 代币地址(如果需要多个pair交换,也需要多个代币地址)
address to, // 用户地址
uint256 deadline // 最低时限
) external virtual override ensure(deadline) returns (uint256[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT");
// 确保pair中有足够的代币B被用来兑换
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
); // 代币的兑换
_swap(amounts, path, to);
}

function swapTokensForExactTokens(
//(确定想要换取代币B的数量,系统给出对应的代币A的数量)
uint256 amountOut,
uint256 amountInMax,
address[] calldata path,
address to,
uint256 deadline
) external virtual override ensure(deadline) returns (uint256[] memory amounts) {
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, "UniswapV2Router: EXCESSIVE_INPUT_AMOUNT");
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}

function swapExactETHForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline)
// 确定的ETH换代币
external
payable
virtual
override
ensure(deadline)
returns (uint256[] memory amounts)
{
require(path[0] == WETH, "UniswapV2Router: INVALID_PATH");
amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
require(amounts[amounts.length - 1] >= amountOutMin, "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT");
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
}

function swapTokensForExactETH(
// 代币来换确定的ETH
uint256 amountOut,
uint256 amountInMax,
address[] calldata path,
address to,
uint256 deadline
) external virtual override ensure(deadline) returns (uint256[] memory amounts) {
require(path[path.length - 1] == WETH, "UniswapV2Router: INVALID_PATH");
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, "UniswapV2Router: EXCESSIVE_INPUT_AMOUNT");
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}

function swapExactTokensForETH(
// 确定代币换取ETH
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external virtual override ensure(deadline) returns (uint256[] memory amounts) {
require(path[path.length - 1] == WETH, "UniswapV2Router: INVALID_PATH");
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT");
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}

function swapETHForExactTokens(uint256 amountOut, address[] calldata path, address to, uint256 deadline)
//ETH换取确定代币
external
payable
virtual
override
ensure(deadline)
returns (uint256[] memory amounts)
{
require(path[0] == WETH, "UniswapV2Router: INVALID_PATH");
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= msg.value, "UniswapV2Router: EXCESSIVE_INPUT_AMOUNT");
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
// refund dust eth, if any
if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
}

swap中手续费收取

手续费:0.3%

推导 SwapExactTokens (未考虑手续费)

假设当前池子储备:

  • ( x_0 ):输入代币储备
  • ( y_0 ):输出代币储备

用户输入 ( dx ),希望换出 ( dy )。

根据恒定乘积公式:

$$
(x_0 + dx)(y_0 - dy) = k = x_0 \times y_0
$$

展开:

$$
x_0 y_0 - x_0 dy + dx y_0 - dx dy = x_0 y_0
$$

两边约掉 ( x_0 y_0 ),整理得:

$$

  • x_0 dy + dx y_0 - dx dy = 0
    $$

忽略高阶小量 ( dx \cdot dy )(即 ( dx ) 很小,相比乘法可以忽略),得到:

$$

  • x_0 dy + dx y_0 \approx 0
    $$

即:

$$
dy = \frac{dx \times y_0}{x_0 + dx}
$$


上述推导是在没有考虑手续费(如 0.3%)时的基础版数学公式。

加上手续费后,只需要把 ( dx ) 乘上 ( 1 - f ) (如 0.997)再代入即可。

上图是没考虑手续费的计算

如果考虑手续费,pari能够拿到的手续费是(1-0.003)*dx = 0.997 dx

推导 getAmountOut 公式

考虑手续费后的交换公式:

$$
dy = \frac{(1 - f) \cdot dx \cdot y_0}{x_0 + (1 - f) \cdot dx}
$$

其中:

  • ( f ) 是手续费比例(通常是 0.003)
  • ( dx ) 是输入的 token 数量
  • ( dy ) 是输出的 token 数量
  • ( x_0, y_0 ) 是当前池子的储备量

如果手续费 ( f = 0.003 ),那么 ( 1 - f = 0.997 ),代入后得:

$$
dy = \frac{0.997 \cdot dx \cdot y_0}{x_0 + 0.997 \cdot dx}
$$

进一步整理,乘以1000/1000方便计算,公式变为:

$$
dy = \frac{997 \cdot dx \cdot y_0}{1000 \cdot x_0 + 997 \cdot dx}
$$


上图推导的是考虑手续费后的 ( dy ),也是 Uniswap V2 源码中 getAmountOut 函数实现的数学依据。

上图才是考虑手续费的dy。同时这个一个数学公式也是在代码中的getAmountOut函数的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
internal
pure
returns (uint256 amountOut)
{
require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");
// 检查输入的金额必须大于零
require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
// 池子内的两种代币必须存在
uint256 amountInWithFee = amountIn.mul(997); // 减去手续后的dx
uint256 numerator = amountInWithFee.mul(reserveOut); // 数学公式中分子的部分
uint256 denominator = reserveIn.mul(1000).add(amountInWithFee); // 数学公式中分母的部分
amountOut = numerator / denominator; // 最终得出dy
}

当我们知道dy来求dx (getAmountIn)

推导 getAmountIn 公式(已知 dy,求 dx)

当前池子储备:

  • ( x_0 ):输入代币储备
  • ( y_0 ):输出代币储备

目标:想要获得 ( dy ) 个输出代币,需要投入多少 ( dx )。

根据恒定乘积公式:

$$
(x_0 + dx)(y_0 - dy) = k = x_0 \times y_0
$$

展开:

$$
x_0 y_0 - x_0 dy + dx y_0 - dx dy = x_0 y_0
$$

约去 ( x_0 y_0 ):

$$

  • x_0 dy + dx y_0 - dx dy = 0
    $$

移项:

$$
dx (y_0 - dy) = x_0 dy
$$

得到:

$$
dx = \frac{x_0 \times dy}{y_0 - dy}
$$


如果考虑手续费 ( f )(通常 ( f = 0.003 ),即千分之三),输入的 ( dx ) 只有 ( (1 - f) \times dx ) 生效。

因此,实际需要投入的 ( dx ) 满足:

$$
(1 - f) \times dx = \frac{x_0 \times dy}{y_0 - dy}
$$

两边同时除以 ( 1 - f ),得到最终公式:

$$
dx = \frac{x_0 \times dy}{(y_0 - dy) \times (1 - f)}
$$

假设手续费 ( f = 0.003 ),则 ( 1 - f = 0.997 ),代入后可具体写成:

$$
dx = \frac{1000 \times x_0 \times dy}{997 \times (y_0 - dy)}
$$


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut)
internal
pure
returns (uint256 amountIn)
{
require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT");
require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
// 和 getAmountOut一样的两项检查
uint256 numerator = reserveIn.mul(amountOut).mul(1000);
// x0 * dy *100(公式中的分子)
uint256 denominator = reserveOut.sub(amountOut).mul(997);
// (y0-dy)*997 数学公式中的分母
amountIn = (numerator / denominator).add(1); //向上取整
}

Uniswap 增减流动性

增加流动性

其中必须要先符合恒定做市商这一公式

image-20250425181656636

image-20250425181740644

其中增加流动性的数学逻辑主要按照上面的公式

先介绍增减流动性是如何在Uniswap里面实现的:

  1. 用户调用路由合约(addliquidity),然后路由合约会进行一个查询(查询工厂合约 getPair),
  2. 两种情况一种是交易对被创建,一种是没有被创建。
  3. 如果交易对没有被有被创建(createPair)
  4. 转账(transferfrom)
  5. 转账成功之后,路由合约调用mint函数
  6. transfer增发 lp token

image-20250424200336425

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
// **** ADD LIQUIDITY ****
function _addLiquidity(
address tokenA, // 要添加的代币
address tokenB, // 要添加的代币
uint256 amountADesired, // dx
uint256 amountBDesired, // dy
uint256 amountAMin,
uint256 amountBMin
) internal virtual returns (uint256 amountA, uint256 amountB) {
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
(uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
// 读取pair合约的各种代币数量(x0 和 x2)
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
// 如果是首次添加流动性(池子为空),直接使用用户提供的数量初始化储备
// 此时 dx/dy 即为初始价格
} else {
uint256 amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
// 如果想要投入dx 的代币A ,按照必须需要投入多少的代币b(amountBOptimal)
if (amountBOptimal <= amountBDesired) {
// 如果预期数量B 大于 当前pair的代币 B

require(amountBOptimal >= amountBMin, "UniswapV2Router: INSUFFICIENT_B_AMOUNT");
// 如果我们投入的dy 没有达到 预期的数量B 就回滚 ,为了保证(dx/x0 = dy/d0)
(amountA, amountB) = (amountADesired, amountBOptimal);
// 因为如果我们一开始计算的期望代币B大于pair 我们输入的代币A也必定大于当前pair的代币A
// 然后我们 我们这样就得出我们当前的 dx 和 dy了
} else {
uint256 amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
// 如果想要投入dy 的代币B ,按照必须需要投入多少的代币A(amountAOptimal)
assert(amountAOptimal <= amountADesired); // 必须为真,否则回滚和消耗所有gas
//期望 A 必须小于 dx 为了保证 恒定做事商
require(amountAOptimal >= amountAMin, "UniswapV2Router: INSUFFICIENT_A_AMOUNT");
// 期望值必须大于等于 dx 否则回滚
(amountA, amountB) = (amountAOptimal, amountBDesired);
// 然后我们 我们这样就得出我们当前的 dx 和 dy了
}
}
}

function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to, // 我们的地址
uint256 deadline // 最低时限,如果在这个之后还没被执行,直接回滚
) external virtual override ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}

function addLiquidityETH(
address token,
uint256 amountTokenDesired,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
)
external
payable
virtual
override
ensure(deadline)
returns (uint256 amountToken, uint256 amountETH, uint256 liquidity)
{
(amountToken, amountETH) =
_addLiquidity(token, WETH, amountTokenDesired, msg.value, amountTokenMin, amountETHMin);
address pair = UniswapV2Library.pairFor(factory, token, WETH);
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pair, amountETH));
liquidity = IUniswapV2Pair(pair).mint(to);
// refund dust eth, if any
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}

关于pair的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, "UniswapV2: IDENTICAL_ADDRESSES");
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), "UniswapV2: ZERO_ADDRESS");
require(getPair[token0][token1] == address(0), "UniswapV2: PAIR_EXISTS"); // single check is sufficient
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
// 联系增加流动性来说,当一个pair没有被创建,我们需要创建一个pari,但是新创还能得pari我们还不确认,
// 我们需要一个确认的地址。所以利用create2进行创建。
}
IUniswapV2Pair(pair).initialize(token0, token1); // pair 初始化 输入交易对
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit

联系增加流动性来说,当一个pair没有被创建,我们需要创建一个pari,但是新创还能得pari我们还不确认, 我们需要一个确认的地址。所以利用create2进行创建。

移除流动性

  1. 用户调用移除流动性函数(removeLiquidity) router合约中
  2. transferFrom转移lptoken给pari合约
  3. 然后pari burn掉lp
  4. transfer dx/dy 给用户

image-20250425182255044

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
function removeLiquidity(
address tokenA, //代币A
address tokenB, // 代币B
uint256 liquidity, // 用户当前的流动性
uint256 amountAMin, // dx
uint256 amountBMin, // dy
address to, // pair 地址
uint256 deadline // 最低时限
) public virtual override ensure(deadline) returns (uint256 amountA, uint256 amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); // 读取当前的pair
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
// 对应的第二步转账 lpt 给pair
(uint256 amount0, uint256 amount1) = IUniswapV2Pair(pair).burn(to);
// 对应第三步 销毁lpt
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
// 对应第四步,转账给用户两种代币。
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
// 将两种代币匹配到正确位置
require(amountA >= amountAMin, "UniswapV2Router: INSUFFICIENT_A_AMOUNT");
require(amountB >= amountBMin, "UniswapV2Router: INSUFFICIENT_B_AMOUNT");
// 确保所有返还的代币满足最小值的要求,防止滑点过大
}
function burn(address to) external lock returns (uint256 amount0, uint256 amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
uint256 balance0 = IERC20(_token0).balanceOf(address(this));
uint256 balance1 = IERC20(_token1).balanceOf(address(this));
uint256 liquidity = balanceOf[address(this)];
// 一般情况下pair 不会包含lpt 所以直接读取当前合约余额就可以知道下回多少lpt

bool feeOn = _mintFee(_reserve0, _reserve1);
uint256 _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
// dx * x0/T
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
// dy * y0 / T
require(amount0 > 0 && amount1 > 0, "UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED");
// 确保预期转走的代币都大于零
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));

_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint256(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}


其中burn里面的数学计算公式也和上面图片中的一样,所以数学公式这么进行的

手续费机制

1 通过增发share(流动性提供者在池子的中的份额)的方式把手续费给项目方

image-20250401202106622

2通过使s1增值的方式把手续费给LP

image-20250401202155528

3 想分走手续费里面的一定比例(这个是uiswap中的手续费计算方式)

image-20250401203056251

image-20250401203216112

直接导入关于手续费机制最关键的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0); // 如果feeOn 为true 手续费就需要交给协议方一部分
uint256 _kLast = kLast; // gas savings <K>
if (feeOn) {
if (_kLast != 0) {
uint256 rootK = Math.sqrt(uint256(_reserve0).mul(_reserve1));
uint256 rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
uint256 numerator = totalSupply.mul(rootK.sub(rootKLast)); // 根号下 k2 - k1
uint256 denominator = rootK.mul(5).add(rootKLast);
uint256 liquidity = numerator / denominator; //// 计算协议方取走手续费的比例
if (liquidity > 0) _mint(feeTo, liquidity); //转移属于协议方的手续费
}
}
//
} else if (_kLast != 0) {
kLast = 0;
}
}

其中这个函数的逻辑就是围绕着一个数学公式来进行的

image-20250422202125206

1.Uniswap V2 中并无“主动分红”机制

其中流动性提供者如果想瓜分手续费需要直接在burn函数里面进行一个提取:

Uniswap V2 并不会在每个区块或固定周期将手续费打到 LP 地址上;所有 Swap 手续费(0.30%)都会直接留在池子储备中,自动增厚池内资产,使得 LP Token 本身的内在价值随之提升 Uniswap Docs | Uniswap

2. 手续费如何累积到 LP Token

  • 费用留存:每次 Swap 时,从输入金额中扣除的手续费不单独转账,而是留在池子的 reserve0/reserve1 中,令 x×yx\times yx×y 增大 Uniswap Docs | Uniswap
  • LP Token 增值:LP Token 代表你在池中的份额,当池子储备增加时,你持有的每个 LP Token 所对应的底层代币数量也会同步增加,这就相当于“被动赚取”了手续费收益 Uniswap Docs | Uniswap

3. 提取手续费的唯一途径:销毁(Burn)LP Token

  • 调用 removeLiquidity:流动性提供者需将手中一定数量的 LP Token 发送回 Pair 合约,触发 burn(),才能按所销毁 LP Token 的比例取回包含手续费在内的两种代币 support.uniswap.org
  • 可部分或全部提取:在界面或 Router 调用时,可指定任意百分比的 LP Token(例如 10%、50% 或 100%)进行销毁,并非只能一次性提取所有 LP Token。

Uniswap 中的无常损失

(为什么做lp会亏钱)

假设一个池子中, 一个交易对是存在的(dai/eth),

初始的lp: 100 dai / 1eth

Pe = y/x = 100dai/eth 则当前(100 dai +eth ) = 200 dai

  1. 假设ETH涨价 根据 x*y = k 100 *1 = 120 *0.83

​ Pe = 120/0.83 = 144.58dai

对于lp来说,120 dai + 0.83 *144.58 = 240 dai;

但是如果不对lp来说会有 100 dai + 1ETH = 100 + 144.58 = 244.58 dai

所以对于 lp来说少赚了 244.58 - 240 = 4.58 dai

所以lp少赚的这一部分就是无常损失

  1. 假设ETH降价了

100 dai :1ETH

x*y = k = 80 dai :1.25 ETH

100 * 1 = 80 *1.25 = 100

Pe = 80 /1.25 = 64 dai/ETH

最一开始的资产有 200 dai

如果降价后会有(lp) : 160 dai

其中如果没有做lp : 100 +1 * 64 = 164 dai

所以lp 如果ETH涨价就会赚的少,如果降价就会亏的多

假设一个池子(tokenA : ETH ,tokenB: dai)

其中x代表池子里ETH的数量

y代表池子中dai的数量

P = y/x = 100 /1 = 100 dai/ETH
$$
x \cdot y = k \
k = L^2 \
\Rightarrow L = \sqrt{xy}
$$
其中 由上面俩公式得到

行内公式:$y = L\sqrt{P}$,$x = \dfrac{L}{\sqrt{P}}$

或者使用块级公式:

$$
y = L\sqrt{P} \
x = \dfrac{L}{\sqrt{P}}
$$
在交换的时候L不变,但是价格会变所以这就是导致无常损失产生的原因

ImpermenantLoss

IL = 做LP导致的损失/ 不做lpde token价值
$$
IL = \frac{\text{做 LP 导致的损失}}{\text{不做 LP (hold) 的 token 价值}} = \frac{V_L - V_{hold}}{V_{hold}}
$$

$$
V_L = y + x \cdot P_1 = L \sqrt{P_1} + \frac{L}{\sqrt{P_1}} \cdot P_1 = 2L \sqrt{P_1}
$$

📘 Impermanent Loss(无常损失)推导

Impermanent Loss(简称 IL),是指作为 LP(流动性提供者)因价格波动而造成的相对损失。


🧮 定义

无常损失的定义如下:

$$
IL = \frac{V_L - V_{\text{hold}}}{V_{\text{hold}}}
$$

其中:

  • ( V_L ):在 ( t_1 ) 时刻做 LP 后,池子中 token 的价值
  • ( V_{\text{hold}} ):在 ( t_1 ) 时刻如果不做 LP,仅持有原 token 的价值

📐 LP 情况下的资产价值

设初始状态下 LP 投入:

  • ( x = \frac{L}{\sqrt{P}} )
  • ( y = L\sqrt{P} )

其中 ( P ) 是 token 的价格,( L ) 是常数(流动性大小的表达式)。

那么在时间 ( t_1 ) 时,价格变为 ( P_1 ),此时:

$$
V_L = y + x \cdot P_1 = L \sqrt{P_1} + \frac{L}{\sqrt{P_1}} \cdot P_1 = 2L \sqrt{P_1}
$$


📦 HOLD 情况下的资产价值

设:

  • 初始价格为 ( P_0 )
  • 价格变化因子为 ( d ),即 ( P_1 = P_0 \cdot d )

持有的是初始的 ( x_0 ) 和 ( y_0 ),它们分别为:

  • ( x_0 = \frac{L}{\sqrt{P_0}} )
  • ( y_0 = L \sqrt{P_0} )

在 ( t_1 ) 时刻:

$$
V_{\text{hold}} = y_0 + x_0 \cdot P_1 = L\sqrt{P_0} + \frac{L}{\sqrt{P_0}} \cdot P_0 \cdot d
= L\sqrt{P_0} + L\sqrt{P_0} \cdot d
= L\sqrt{P_0} (1 + d)
$$


🔍 IL 最终表达式

$$
IL = \frac{2L\sqrt{P_1} - L\sqrt{P_0}(1 + d)}{L\sqrt{P_0}(1 + d)}
$$

由于 ( P_1 = P_0 \cdot d ),代入得到:

$$
IL = \frac{2L\sqrt{P_0 d} - L\sqrt{P_0}(1 + d)}{L\sqrt{P_0}(1 + d)}
= \frac{2\sqrt{d} - (1 + d)}{1 + d}
$$

这是经典的无常损失表达式,仅与价格变化比 ( d ) 有关。


✅ 小结

  • IL 越大,表示做 LP 相比于单纯持币越吃亏
  • 当价格回归原位(即 ( d = 1 )),则 ( IL = 0 ),不存在无常损失
  • 在 Uniswap V2 恒定乘积市场中,无常损失是不可避免的

image-20250425194926953

flashloan(闪电贷)

闪电贷就是一种无抵押贷款,借款人必须在区块链上的同一比交易中偿还资产。更幼稚的解释就是:可以让你借到100万但是立马归还、

什么是闪电贷?

闪电贷是无担保(无抵押)贷款,借款人必须在同一笔交易中向贷方偿还全部贷款。它们是独特的金融产品,仅在 DeFi 世界中可用,因为智能合约可以强制用户立即偿还贷款。相比之下,传统金融中不存在这样的原始结构。AaveDyDx 等 DeFi 协议支持闪电贷。人们认为 MakerDAO 和 Uniswap 等协议也支持闪电贷,但从技术上讲,它们是“闪电铸币”,非常相似。

说明闪电贷机制的图表,包括存款、合同、资产变动、费用和合同。

  1. 贷方(上图中的 Whale)决定借出 1,000 美元 USDC。

    1. 他们将 1,000 美元存入一个智能合约中,该合约包含闪电贷的代码。
    2. 他们听说,每次有人将他们的钱用于闪电贷时,他们都会得到报酬,而且他们希望得到报酬。
  2. 用户(上图中为 MetaMask)决定要申请闪电贷(原因见下文)。

  3. 在单个交易中,用户 (MetaMask) 调用智能合约上的一个函数,该函数“一次全部”或“完全不执行”执行以下操作:

    1
    flashloan
    1. 用户 (Metamask) 获得 1,000 美元 USDC
    2. 他们想用它做什么就做什么(同样,仍然在同一笔交易中)
    3. 然后,他们偿还 1,000 美元 + 一小笔费用

就是这样。

原子事务

所有区块链交易都是所谓的,因为要么全部发生,要么都没有发生。此属性也适用于 ,其中所有闪电贷都是原子的。在这种情况下,Atomic 意味着如果用户没有立即偿还贷款,他们就从未获得过贷款。

1
2
3
4
5
6
7
8
9
10
11
12
function flashLoan(uint256 amount) external {
uint256 balanceBefore = token.balanceOf(address(this));

token.transfer(msg.sender, amount);
// Ignore IFlashLoanReceiver for this pseudo-code
IFlashLoanReceiver(msg.sender).execute();

if (token.balanceOf(address(this)) < balanceBefore) {
revert RepayFailed();
}
}

调用函数的用户本质上将调用在智能合约中如下所示的函数。如果代码命中线路,则整个交易不会成功或完成,这意味着用户从一开始就没有借到钱!

智能合约的工作原理是这样的:每当点击对账单时,区块链都会自动将交易的所有状态更改直接从交易恢复到其原始状态。revert

  • 您是否打印了 1,000,000,000 美元,但遇到了回退线?→ 您什么都没打印
  • 你做了一生的交易,但又打了个反头?→ 你不是 Keith Gill
  • 你向失散多年的亲戚发送了一条链上消息,在疏远多年后终于重新联系上,这是你唯一的一次尝试,但命中了回退?→ 悲剧爱陪伴

闪电贷 EIP-3156 - 获取技术

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @dev Initiate a flash loan.
* @param receiver The receiver of the tokens in the loan, and the receiver of the callback.
* @param token The loan currency.
* @param amount The amount of tokens lent.
* @param data Arbitrary data structure, intended to contain user-defined parameters.
*/
function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external returns (bool);

通常,外部拥有的钱包(又名非智能合约钱包,在我们的图片中又名 Metamask)不会出现在此类函数调用中。相反,闪电贷合约会将借入的代币发送到另一个智能合约,该合约通常具有“做”事的功能。receiver

Diagram of a flash loan and how it interacts with the core contract, the receiver's contract, and replayment.

闪电贷交互图。

闪电贷如何使用?作用是啥?

在实践中,闪电贷的使用原因通常与普通贷款类似。最常见的是“获得杠杆”或资本以获得以下机会:

  1. 套汇
  2. 清算
  3. 抵押品掉期
  4. 其他 MEV

什么是套利?

套利是一种利用同一资产在不同市场的价格差异的金融策略。想象一下,eBay 和 Amazon(在线经销商)以 5 美元的价格出售苹果,而阿里巴巴(另一家在线经销商)以 1 美元的价格出售苹果。如果你有 100 美元,什么是简单的赚钱方法?

  1. 在阿里巴巴上购买 100 个苹果(每个苹果 1 美元 * 100 个苹果 = 100 美元成本)
  2. 在 eBay 和亚马逊上出售这 100 个苹果(100 个苹果 * 每个苹果 5 美元 = 500 美元利润)
  3. 您刚刚赚了 400 美元!(500 美元的利润 - 100 美元的成本)

这种类型的金融策略几乎存在于世界上每个市场,但利润率通常非常微薄。

在 DeFi 中,像 Uniswap 这样的去中心化交易所存在这样的机会。

Diagram illustrating how arbitrage works.

说明套利如何运作的图表。

现在,让我们以 1 美元和 5 美元的苹果为例。我们只花了 100 美元买苹果,因为这就是我们所拥有的全部,但如果我们有更多的钱,我们本可以获得更大的利润。这就是闪电贷的用武之地。

闪电贷套利

让我们再次回顾上述情况,但想象一下我们可以先进行闪电贷后进行此操作。

  1. 我们从闪电贷合同中借入 1,000 美元并开始交易。
    1. 请记住,我们必须在同一笔交易中偿还!
    2. 所以,在同一笔交易中,我们用 1,000 美元从阿里巴巴购买了 1,000 个苹果。
    3. 然后,我们立即在 Amazon 和 eBay 上以 5,000 美元的价格出售这 1,000 个苹果,赚了 5,000 美元!
    4. 然后,我们偿还了最初的 1,000 美元,以闪电贷的形式取出。由于贷款已偿还,因此交易不会恢复!(通常,您还必须支付少量费用,可能是 1 美元。
  2. 最后,交易结束了,我们净赚了 4,000 美元(减去小额费用),而不是 400 美元!
    1. 5,000 美元的销售额 - 1,000 美元的贷款偿还到闪电贷合同中
  3. 而我们自己却没有钱就完成了这一切!

这就是闪电贷的力量。任何人,即使没有抵押品,也可以利用套利机会。

Diagram illustrating how arbitrage works with flash loans.

amountOut: dx0(用户借出的)

amountIn:dx1(用户还来的) = dx0 + fee

fee = 0.003 (dx0 +fee)

0.997fee = 0.003dx0

fee = 3/997 dx0

amountIn -fee = 0.997dx1

fee = 3/997 dx0

站在池子的角度

x0 - dx0 +0.997dx1 >= x0

x0 : 池子里的某种代币

dx0 :用户借出的

0.997dx1 :用户还来的

dx1= dx0 + fee 代入得

闪电贷在Uiswap的实现过程

pair合约 swap方法(amountout,amountIn,to,data(闪电贷的实现方法))

    1. 用户需要自己先实现一个闪电贷合约(此合约需要满足一些接口条件(IuniswapV2Callet)
    2. 调用swap(pair合约) 然后pair
    3. pair 合约调用闪电贷的UniswapV2callet
    4. 还钱(dx0 +fee)

image-20250426164529190

其中这四个步骤都需要在同一个交易里面进行

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
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external lock {
// data 闪电贷的data
require(amount0Out > 0 || amount1Out > 0, "UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT");
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, "UniswapV2: INSUFFICIENT_LIQUIDITY");

uint256 balance0;
uint256 balance1;
{
// scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, "UniswapV2: INVALID_TO");
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
// 检查是否有闪电贷
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
uint256 amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint256 amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, "UniswapV2: INSUFFICIENT_INPUT_AMOUNT");
{
// scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint256 balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint256 balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); // 收取手续费
require(
balance0Adjusted.mul(balance1Adjusted) >= uint256(_reserve0).mul(_reserve1).mul(1000 ** 2),
"UniswapV2: K"
);
}

_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}