自动做市商(AMM)

笔者在Uniswap-v1原理和源码分析 - Salbt’s blog中对AMM进行一些粗略的解释,以下我们将进一步阐述AMM在交易所中的应用和各种类型的衍生。

相关概念

流动性(Liquidity):在AMM中的流动性是指将一种资产转化为另一种资产,这里资产可以是以太坊上的erc20代币

流动性池(Liquidity pools):交易的代币池,可以理解为存放两个代币储备金的金库。交易者可以自由在金库中进行代币的买卖。

流动性提供者(Liquidity Provider,LP):LP也可认为是做市商,LP可以根据池中代币权重添加储备金。他们通过向池子中交易收取交易费来获取利润。

去中心化交易所

去中心化交易所(Decentralized Exchange,简称DEX)是一种无需中央机构进行控制和管理的加密货币交易平台。在去中心化交易所中,交易通过智能合约直接在区块链上执行,用户可以在没有中介的情况下进行点对点交易。

在当前公链上有两种方式实现的DEX:

1.基于订单薄的DEX

  • CLOB模式(Central Limit Order Book,中心限价订单簿),是一个出价和报价组成的权限透明账本。能够了解交易参与者买卖价格。

  • 卖家可以以指定价格出售加密货币,买家可以以指定价格购买加密货币。当前加密货币价格是买卖价格集中那个区间的值。

  • 订单薄模式在链上运行有明显的缺点。对于像以太坊这样TPS较低的公链,很难满足订单薄高频的交易需求

    这绝对是致命的缺点,有兴趣的小伙伴可以去了解下dydx,dydx最早就是在以太坊上使用订单薄模式的DEX。

订单簿

上图是比特币2024.5.12 12:11左右的价格

2.基于流动性的DEX

  • 将流动性池作为DEXs智能合约上的代币储备(reserve)。
  • AMM以一种算法的形式预测资产价格,链上的DEXs大多都式用AMM。
  • 为DEX添加流动的LP可以得到DEX的LPT(Liquidity Provider Token, LPT)作为奖励,LP可以通过出售增值的LPT获取收益。

各种类型的AMM

如今链上主要的DEX有:uniswap、curve、pancakeSawp、Raydium、Balancer。

想知道更多DEX可以在defillama中获取

现有流行的AMM类型数学函数如下:

1.恒定乘积做市商公式:x * y = k

  • Uniswap、PancakeSwap、还有solana链上的Raydium都是恒定乘积的使用者,使用该算法的池子的流动性由两个代币的乘积获得。
  • 当x供应增加,y的供应就会减少,从而导致流动性k维持在一个恒定的值不变。
  • 当交易规模变大时,可能会出现巨大的滑点,我们将在稍后详细讲解滑点。

2.恒定和做市商公式:x + y = k

  • 恒定和在图像上表现为一条直线,它是零滑点的理想模型(它的成交均价为k,即任何一处交易曲线上发生的交易的其斜率不变)。
  • 对比恒定乘积公式,恒定和不能提供无限的流动性。套利者可以耗尽流动性池中全部的储备,流动性池也不能为其他交易者留下可用流动性。这种模式不适合大多数AMM使用。

3.恒定加权乘积做市商:

恒定加权乘积

  • Balancer是恒定加权乘积的使用者。恒定加权乘积是恒定乘积的一个变种,它允许在流动性池中添加多个代币,并使用该公式预测多个代币之间的价格。
  • 它的优点是,交易者能够在池中进行任意资产的互换。

balancer的相关资料可以参考者篇文章:万字解析dYdX发展史:为何放弃L2,决意自建L1? | CoinVoice

4.稳定交换公式:

稳定交换

  • 该公式由Curve推广,是恒定乘积与恒定和的混合体。
  • 当用户资产组合相对平衡时,交易会发生在恒定和曲线上;当不平衡时,交易会切换到恒定乘积曲线上。
  • 这个公式允许较低的滑点和无偿损失,但只适用于具有类似价值的资产,因为所需交易范围的价格总是接近于1。

AMM带来的风险

AMM也不全部是优点,对于交易者和LP来说也有一定的风险:

滑点

如何理解滑点(Price Slippage)?

我们可以通过Uniswap的AMM机制举例。我们能知道,Uniswap中两种token的价格会在一条曲线上变化,当交易订单越大(对于池子中代币总量来说很大)产生的滑点就越大,可从下图中看出:

  • 假如我们拥有一个x*y = 4的流动新池。我们的初始价格为3.96 USD/ETH(图中橙色虚线),此时ETH储备大约为1 ,USD储备大约为3.96 。
  • 我们想卖出大约3.28 USD/ETH个,可以从图中看出,这笔大交易使ETH价格从3.96降到了0.21 USD/ETH。
  • 我们实际相当于支付的均价为:0.92 USD/ETH,这是我们初始价格的0.23倍左右。

desmos-graph

上面我们用一个小流动性的池子和一个大交易,展示了滑点的产生过程。

在DEX中一般可以设置滑点容忍度,比如:我通过1 ETH换取2000 DAI,此时我能接受大约0.5%的滑点,那么我卖出1 ETH至少能获得2000 * 0.995 = 1990 DAI。

Front-Run和Back-Run

接下来讲一讲抢跑交易(Front-Run)和尾随交易(Back-Run)。公链上的交易都是公开的,所有人都能够看到,所以任何都能够监视区块链上的交易。投机者(一般web3称他们为科学家)能够根据这些公开的交易在以太坊上进行操作从而牟利。

Front-Run:投机者在知道某个用户还未上链的交易后,会发起一个更高gas的交易。矿工会优先打包gas费高的交易,所以投机者的交易排序会先于这个用户的交易。

Back-Run:它的逻辑和Front-Run类似,投机者会某个用户还未上链的交易后发起一个较低gas的交易,矿工会将该交易在用户交易之后打包进区块。

你可能隐约知道了这些操作怎样去进行获利了,下面我通过一个Sandwich attack来具体分析Front-Run和Back-Run怎样去提取MEV(Maximal Extractable Value,最大可提取价值)。

三明治攻击(Sandwich attack)

三明治攻击是什么?

三明治攻击和它的名字一样,攻击者会发送两笔交易将用户的交易夹在中间,通过Front-Run和Back-Run这两笔交易操作进行牟利。

![sandwich attack](/images/posts/AMM/sandwich attack.jpg)

好了,现在我们将会在Uniswap上发起一次Sandwich attack。它会使用到Front-Run和Back-Run的逻辑提取MEV。

  • 假设,我们有一个监视区块链的机器人,它会监视Uniswap中大交易。
  • 机器人在监控的池子状态:eth-1000,usdt-3500000,liquidity-1000*3500000
  • 此时有个交易者想要卖出20个eth,这个交易被我们的机器人检测到了。
  • 于是我们发送两个交易:Front-Run和Back-Run
  • Front-Run会抢先在交易者前,卖出5个eth。Back-Run在交易者后用卖出的eth得到的17412.936usdt去购买eth。
  • 最终我们盈利了0.20046997eth

下面使用go简单的模拟上述过程,实际操作中需要考虑当前网络gas花费、各个流动池手续费差异、是否有竞争者这些因数。

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
package main

import (
"fmt"
)

type Pool struct {
x, y float32
k float32
}

func main() {
p := NewPool(1000, 3500000)
fmt.Println(p, "价格:", p.Price())

var cost float32 = 5
getUsdt := p.SwapXtoY(cost)
fmt.Println("投机者获得usdt:", getUsdt, " 价格:", p.Price())
p.SwapXtoY(20)
fmt.Println(p, "价格:", p.Price())
getEth := p.SwapYtoX(getUsdt)
fmt.Println("投机者净盈利eth: ", getEth-cost)
}

func NewPool(x, y float32) *Pool {
return &Pool{
x: x,
y: y,
k: x * y,
}
}

func (p *Pool) SwapXtoY(dx float32) float32 {
X := p.x
Y := p.y

dy := dx * Y / (dx + X)

p.x += dx
p.y -= dy
p.k = p.x * p.y
return dy
}

func (p *Pool) SwapYtoX(dy float32) float32 {
X := p.x
Y := p.y

dx := dy * X / (dy + Y)

p.x -= dx
p.y += dy
p.k = p.x * p.y
return dx
}

func (p Pool) Price() float32 {
return p.y / p.x
}

func (p Pool) String() string {
return fmt.Sprint("池子当前状态: (tokenA: ", p.x, ", tokenB: ", p.y, ", liquidity: ", p.k, ")")
}

How to DeFi中展示了以太坊上的sandwich attack案例

sandwichDemo

无偿损失

作为AMM中的LP,你可能会面临无偿损失。无常损失类似于衡量你在资金池中持有代币与在钱包中持有代币的机会成本。如果,你的代币还在流动性池中,那么无偿损失就还不会实现,只有当你移除流动性将代币拿出是才会实现。

我们可以继续用上面的代码来计算无偿损失。

  • 假如,我们持有eth-10,usdt-35000,我们选择在uniswap中做市商。
  • 然后有交易者在我们池子中卖出3个eth,此时价格从3500 滑倒了 2071
  • 根据池子状态计算当前池子中总价值:53846
  • 如果我们不做市商,那么我们持有代币时总价值应该为:55710
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
func main() {

holdx := float32(10)
holdy := float32(35000)

p := NewPool(holdx, holdy)
fmt.Println(p, "价格:", p.Price())
// 池子当前状态: (tokenA: 10, tokenB: 35000, liquidity: 350000) 价格: 3500

p.SwapXtoY(3)
fmt.Println(p, "价格:", p.Price())
// 池子当前状态: (tokenA: 13, tokenB: 26923.078, liquidity: 350000) 价格: 2071.006

poolV := p.Price()*p.x + p.y
fmt.Println("池中总价值:", poolV)
// 池中总价值: 53846.156

holdV := holdx*p.Price() + holdy
fmt.Println("如果持有代币总价值:", holdV)
// 如果持有代币总价值: 55710.062

lostV := holdV - poolV
fmt.Println("损失价值:", lostV, "百分比", lostV/holdV*100, "%")
// 损失价值: 1863.9062 百分比 3.3457265 %

}

计算无偿损失的公式

无偿损失

可以用go检验一下这个公式

1
2
3
4
d := 2071.006 / 3500
IL := 2*math.Sqrt(d)/(d+1) - 1
fmt.Println(IL)
// -0.03345724411216

参考资料

How to DeFi