Raydium 是一个 Solana 平台上的 DEX,提供了各种代币的流动性池,为用户提供流动性挖矿、购买代币的功能。为了给 DEX 项目引入流动性池、对接 Raydium 的交换协议,对 raydium 恒定乘积交换合约的源码进行深入学习,这也是 Raydium 上最新的标准 AMM。
Repository: https://github.com/raydium-io/raydium-cp-swap
流动性池状态及创建时的基本流程
流动性池状态定义如下:
1 |
|
初始化流动性池的指令函数签名如下:
1 | pub fn initialize( |
初始化流动性池账户定义如下:
1 |
|
初始化流动性池流程如下:
- 判断两种资产的 Mint 账户是否合法,判断 AMM 配置中是否关闭创建 Pool。
- 设定开始交易时间。
- 创建两种资产的金库。
- 创建 PoolState 数据账户。
- 创建 ObservationState 数据账户。
- 将两种初始资产从创建者账户转账到金库账户。
- 判断两个金库账户的数额是否合法(实际上只要大于 0 就合法)。
- 计算流动性值: $liquidity = \sqrt(amount0 * amount1)$。
- 固定锁定 100 个流动性值:
let lock_lp_amount = 100
- 发放
liquidity - lock_lp_amount
个 LP token 给创建者。 - 从创建者账户里收取创建费(lamports)到一个专门存放创建费的账户中(地址硬编码在合约里)。
- 初始化流动性池数据的各个字段。
资产交换
资产交换分为两类:
- 基于输入资产: 输入资产额度固定,一部分会作为手续费,一部分作为购买资金输入。
- 基于输出资产: 输出资产额度固定,需要额外购买一部分输出资产作为手续费,其他作为购买到的资产输出。
基于输入资产
指令函数签名:
1 | pub fn swap_base_input( |
基于输入的资产交换流程如下:
- 检查时间、状态等是否允许交换。
- 计算转账(应该指的是向金库中转账)费用,从总的输入费用中减去这部分,将剩余部分
actual_amount_in
作为实际购买资金。 - 计算两个金库扣除协议费用和资金费用之后剩余的部分,即实际上提供流动性的两种资金。
- 按照上一步计算得到了两种资金,计算两种资金置换另一种的价格,$A / B$ 和 $B / A$,同时转换为
u128
左移 32 位使用定点数来保存精度。 - 将第 3 步得到的两种资金额度相乘,得到恒定乘积 $A * B$。
- 计算交换结果,包括各种额度、费用。
- 将上一步得到的计算结果中的交换后的的源资产额度减去交易费用,再乘以这个结果中交换后的目标资产额度,得到交换后的恒定乘积 $A’ * B’$。
- 验证第 6 步计算结果中交换的源资产额度是否等于
actual_amount_in
。 - 检查第 6 步计算结果中交换得到的目标资产额度减去转账费用之后,是否仍然大于 0 ;同时检查,是否大于等于
minimum_amount_out
,如果不满足表示超过滑点限制。 - 更新流动性池状态的协议费用及资金费用,用于记录金库中非流动性的部分的额度。
- 发送一个
SwapEvent
事件。(为什么?怎么利用?) - 验证交换后的恒定乘积大于等于交换前的恒定乘积,即 $A’ B’ \geq A B$。
- 根据计算后的结果,将相应额度的输入资产从用户账户转账到金库账户,将相应额度的输出资产从金库账户转账到用户账户。
- 更新观测状态(
ObservationState
)的值。
基于输出资产
TODO
观测状态 / TWAP 计价器
观测值及观测状态结构定义如下:
1 |
|
其中最重要的函数是更新:
1 | pub fn update( |
- 该函数将一个 Oracle 观测值写入到当前状态中,并将当前索引自增一模
OBSERVATION_NUM
,即循环写入。 - 该函数一秒钟最多执行一次。
- 将两种资产的当前价格与跟上一个记录的时间差值相乘,即: $Price * \Delta T$。
- 然后将上一步求出的两个结果与上一个记录的两个值累加起来 (
wrapping_add
),作为新记录的两个值。 - 更新索引。
原理
TWAP ,即时间加权平均价格(Time Weighted Average Price);用来平滑价格波动,减少大宗交易对市场价格的冲击。
计算公式为:
即某个特定时间内的 TWAP 为:每小段时间乘以当时的价格求和,除以总时间。
设计理由(猜测)
我能想到的另外一种方案是:
- 观测状态中的每个观测记录,只记录 $Price * \Delta T$ 和时间戳即可。
- 当要求某段时间的 TWAP 时,将这段时间的所有记录累加,除以总时长即可,时间复杂度 $O(n)$。
- 这样看起来好像可以避免
wrapping_add
,源码中不断累加更可能遇到这种情况。
而源码在计算某段时间的 TWAP 时,只需要将最后一个记录的值和第一个记录的值的差除以总时间即可,即这种方案时间复杂度只有 $O(1)$ 。而且实际上 wrapping_add
得到的累加值在相减的时候仍然可以得到正确的结果,只要在这段时间内没有溢出两次就行了。