Skip to content

Commit

Permalink
add dex-dev-0
Browse files Browse the repository at this point in the history
  • Loading branch information
gengteng committed Dec 26, 2024
1 parent 9c2b0f2 commit 36dd583
Showing 1 changed file with 361 additions and 0 deletions.
361 changes: 361 additions & 0 deletions source/_posts/dex-dev-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,361 @@
---
title: DEX 开发笔记 - Raydium 恒定乘积交换合约源码阅读
date: 2024-12-26 20:30:56
tags: ['solana', 'web3', 'dex', 'amm']
mathjax: true
---

Raydium 是一个 Solana 平台上的自动做市商(AMM).为了给 DEX 项目引入流动性池、对接 Raydium 的交换协议,对 raydium 恒定乘积交换合约的源码进行深入学习,这也是 Raydium 上最新的标准 AMM。

> Repository: https://github.com/raydium-io/raydium-cp-swap
## 流动性池状态及创建时的基本流程

流动性池状态定义如下:

```rust
#[account(zero_copy(unsafe))]
#[repr(packed)]
#[derive(Default, Debug)]
pub struct PoolState {
/// 使用的配置,主要包括各种费率,控制标志,拥有者 / 受益者 等信息
pub amm_config: Pubkey,
/// 流动性池创建者
pub pool_creator: Pubkey,
/// Token A 的金库,用来存储流动性 Token 及收益
pub token_0_vault: Pubkey,
/// Token B 的金库,用来存储流动性 Token 及收益
pub token_1_vault: Pubkey,

/// 流动性池 token 会在存入 A 或 B 时发放
/// 流动性池 token 可以被提取为对应的 A 或 B
pub lp_mint: Pubkey,
/// A 的铸币厂
pub token_0_mint: Pubkey,
/// B 的铸币厂
pub token_1_mint: Pubkey,

/// A 使用的 Token 程序,比如旧版本或者 2022
pub token_0_program: Pubkey,
/// B 使用的 Token 程序
pub token_1_program: Pubkey,

/// TWAP 计价账户的地址
pub observation_key: Pubkey,

/// 用于 PDA 签名时使用的 bump
pub auth_bump: u8,
/// Bitwise representation of the state of the pool
/// bit0, 1: disable deposit(vaule is 1), 0: normal
/// bit1, 1: disable withdraw(vaule is 2), 0: normal
/// bit2, 1: disable swap(vaule is 4), 0: normal
pub status: u8,

pub lp_mint_decimals: u8,
/// mint0 and mint1 decimals
pub mint_0_decimals: u8,
pub mint_1_decimals: u8,

/// True circulating supply without burns and lock ups
pub lp_supply: u64,
/// 金库中欠流动性提供者的 A 和 B 的数额.
pub protocol_fees_token_0: u64,
pub protocol_fees_token_1: u64,

pub fund_fees_token_0: u64,
pub fund_fees_token_1: u64,

/// The timestamp allowed for swap in the pool.
pub open_time: u64,
/// recent epoch
pub recent_epoch: u64,
/// padding for future updates
pub padding: [u64; 31],
}
```

初始化流动性池的指令函数签名如下:

```rust
pub fn initialize(
ctx: Context<Initialize>, // 上下文
init_amount_0: u64, // A 资产初始数额
init_amount_1: u64, // B 资产初始数额
mut open_time: u64, // 开始交易时间
) -> Result<()>;
```

初始化流动性池账户定义如下:

```rust
#[derive(Accounts)]
pub struct Initialize<'info> {
/// Address paying to create the pool. Can be anyone
#[account(mut)]
pub creator: Signer<'info>,

/// Which config the pool belongs to.
pub amm_config: Box<Account<'info, AmmConfig>>,

/// CHECK: pool vault and lp mint authority
#[account(
seeds = [
crate::AUTH_SEED.as_bytes(),
],
bump,
)]
pub authority: UncheckedAccount<'info>,

/// CHECK: Initialize an account to store the pool state
/// PDA account:
/// seeds = [
///     POOL_SEED.as_bytes(),
///     amm_config.key().as_ref(),
///     token_0_mint.key().as_ref(),
///     token_1_mint.key().as_ref(),
/// ],
///
/// Or random account: must be signed by cli
#[account(mut)]
pub pool_state: UncheckedAccount<'info>,

/// Token_0 mint, the key must smaller then token_1 mint.
#[account(
constraint = token_0_mint.key() < token_1_mint.key(),
mint::token_program = token_0_program,
)]
pub token_0_mint: Box<InterfaceAccount<'info, Mint>>,

/// Token_1 mint, the key must grater then token_0 mint.
#[account(
mint::token_program = token_1_program,
)]
pub token_1_mint: Box<InterfaceAccount<'info, Mint>>,

/// pool lp mint
#[account(
init,
seeds = [
POOL_LP_MINT_SEED.as_bytes(),
pool_state.key().as_ref(),
],
bump,
mint::decimals = 9,
mint::authority = authority,
payer = creator,
mint::token_program = token_program,
)]
pub lp_mint: Box<InterfaceAccount<'info, Mint>>,

/// payer token0 account
#[account(
mut,
token::mint = token_0_mint,
token::authority = creator,
)]
pub creator_token_0: Box<InterfaceAccount<'info, TokenAccount>>,

/// creator token1 account
#[account(
mut,
token::mint = token_1_mint,
token::authority = creator,
)]
pub creator_token_1: Box<InterfaceAccount<'info, TokenAccount>>,

/// creator lp token account
#[account(
init,
associated_token::mint = lp_mint,
associated_token::authority = creator,
payer = creator,
token::token_program = token_program,
)]
pub creator_lp_token: Box<InterfaceAccount<'info, TokenAccount>>,

/// CHECK: Token_0 vault for the pool, create by contract
#[account(
mut,
seeds = [
POOL_VAULT_SEED.as_bytes(),
pool_state.key().as_ref(),
token_0_mint.key().as_ref()
],
bump,
)]
pub token_0_vault: UncheckedAccount<'info>,

/// CHECK: Token_1 vault for the pool, create by contract
#[account(
mut,
seeds = [
POOL_VAULT_SEED.as_bytes(),
pool_state.key().as_ref(),
token_1_mint.key().as_ref()
],
bump,
)]
pub token_1_vault: UncheckedAccount<'info>,

/// create pool fee account
#[account(
mut,
address= crate::create_pool_fee_reveiver::id(),
)]
pub create_pool_fee: Box<InterfaceAccount<'info, TokenAccount>>,

/// an account to store oracle observations
#[account(
init,
seeds = [
OBSERVATION_SEED.as_bytes(),
pool_state.key().as_ref(),
],
bump,
payer = creator,
space = ObservationState::LEN
)]
pub observation_state: AccountLoader<'info, ObservationState>,

/// Program to create mint account and mint tokens
pub token_program: Program<'info, Token>,
/// Spl token program or token program 2022
pub token_0_program: Interface<'info, TokenInterface>,
/// Spl token program or token program 2022
pub token_1_program: Interface<'info, TokenInterface>,
/// Program to create an ATA for receiving position NFT
pub associated_token_program: Program<'info, AssociatedToken>,
/// To create a new program account
pub system_program: Program<'info, System>,
/// Sysvar for program account
pub rent: Sysvar<'info, Rent>,
}
```

初始化流动性池流程如下:

1. 判断两种资产的 Mint 账户是否合法,判断 AMM 配置中是否关闭创建 Pool。
2. 设定开始交易时间。
3. 创建两种资产的金库。
4. 创建 PoolState 数据账户。
5. 创建 ObservationState 数据账户。
6. 将两种初始资产从创建者账户转账到金库账户。
7. 判断两个金库账户的数额是否合法(实际上只要大于 0 就合法)。
8. 计算流动性值: $liquidity = \sqrt(amount0 * amount1)$。
9. 固定锁定 100 个流动性值: `let lock_lp_amount = 100`
10. 发放 `liquidity - lock_lp_amount` 个 LP token 给创建者。
11. 从创建者账户里收取创建费(lamports)到一个专门存放创建费的账户中(地址硬编码在合约里)。
12. 初始化流动性池数据的各个字段。

## 资产交换

资产交换分为两类:

1. 基于输入资产: 输入资产额度固定,一部分会作为手续费,一部分作为购买资金输入。
2. 基于输出资产: 输出资产额度固定,需要额外购买一部分输出资产作为手续费,其他作为购买到的资产输出。

### 基于输入资产

指令函数签名:

```rust
pub fn swap_base_input(
ctx: Context<Swap>, // 上下文
amount_in: u64, // 输入资产
minimum_amount_out: u64 // 最小输出资产,由调用者按照当前价格及滑点计算得出
) -> Result<()>;
```

基于输入的资产交换流程如下:

1. 检查时间、状态等是否允许交换。
2. 计算转账(应该指的是向金库中转账)费用,从总的输入费用中减去这部分,将剩余部分 `actual_amount_in` 作为实际购买资金。
3. 计算两个金库扣除协议费用和资金费用之后剩余的部分,即实际上提供流动性的两种资金。
4. 按照上一步计算得到了两种资金,计算两种资金置换另一种的价格,$A / B$ 和 $B / A$,同时转换为 `u128` 左移 32 位使用定点数来保存精度。
5. 将第 3 步得到的两种资金额度相乘,得到恒定乘积 $A * B$。
6. 计算交换结果,包括各种额度、费用。
7. 将上一步得到的计算结果中的交换后的的源资产额度减去交易费用,再乘以这个结果中交换后的目标资产额度,得到交换后的恒定乘积 $A' * B'$。
8. 验证第 6 步计算结果中交换的源资产额度是否等于 `actual_amount_in`
9. 检查第 6 步计算结果中交换得到的目标资产额度减去转账费用之后,是否仍然大于 0 ;同时检查,是否大于等于 `minimum_amount_out`,如果不满足表示超过滑点限制。
10. 更新流动性池状态的协议费用及资金费用,用于记录金库中非流动性的部分的额度。
11. 发送一个 `SwapEvent` 事件。(为什么?怎么利用?)
12. 验证交换后的恒定乘积大于等于交换前的恒定乘积,即 $A' * B' \geq A * B$。
13. 根据计算后的结果,将相应额度的输入资产从用户账户转账到金库账户,将相应额度的输出资产从金库账户转账到用户账户。
14. 更新观测状态(`ObservationState`)的值。

### 基于输出资产

TODO

### 观测状态 / TWAP 计价器

观测值及观测状态结构定义如下:

```rust
#[zero_copy(unsafe)]
#[repr(packed)]
#[derive(Default, Debug)]
pub struct Observation {
/// The block timestamp of the observation
pub block_timestamp: u64,
/// the cumulative of token0 price during the duration time, Q32.32, the remaining 64 bit for overflow
pub cumulative_token_0_price_x32: u128,
/// the cumulative of token1 price during the duration time, Q32.32, the remaining 64 bit for overflow
pub cumulative_token_1_price_x32: u128,
}

#[account(zero_copy(unsafe))]
#[repr(packed)]
#[cfg_attr(feature = "client", derive(Debug))]
pub struct ObservationState {
/// Whether the ObservationState is initialized
pub initialized: bool,
/// the most-recently updated index of the observations array
pub observation_index: u16,
pub pool_id: Pubkey,
/// 固定长度的环形缓冲区,用于循环写入观测记录
pub observations: [Observation; OBSERVATION_NUM], // OBSERVATION_NUM == 100
/// padding for feature update
pub padding: [u64; 4],
}
```

其中最重要的函数是更新:

```rust
pub fn update(
&mut self,
block_timestamp: u64,
token_0_price_x32: u128,
token_1_price_x32: u128,
);
```

1. 该函数将一个 Oracle 观测值写入到当前状态中,并将当前索引自增一模 `OBSERVATION_NUM`,即循环写入。
2. 该函数一秒钟最多执行一次。
3. 将两种资产的当前价格与跟上一个记录的时间差值相乘,即: $Price * \Delta T$。
4. 然后将上一步求出的两个结果与上一个记录的两个值累加起来 (`wrapping_add`),作为新记录的两个值。
5. 更新索引。

### 原理

TWAP ,即时间加权平均价格(Time Weighted Average Price);用来平滑价格波动,减少大宗交易对市场价格的冲击。

计算公式为:

$$
\text{TWAP} = \frac{\sum_{i=1}^{n} \left( P_i \times \Delta t_i \right)}{\sum_{i=1}^{n} \Delta t_i}
$$

即某个特定时间内的 **TWAP** 为:每小段时间乘以当时的价格求和,除以总时间。


### 设计理由(猜测)

我能想到的另外一种方案是:

1. 观测状态中的每个观测记录,只记录 $Price * \Delta T$ 和时间戳即可。
2. 当要求某段时间的 TWAP 时,将这段时间的所有记录累加,除以总时长即可。
3. 这样看起来好像可以避免 `wrapping_add`,源码中不断累加更可能遇到这种情况。

而源码在计算某段时间的 TWAP 时,只需要将最后一个记录的值和第一个记录的值的差除以总时间即可,而且实际上 `wrapping_add` 得到的累加值在相减的时候仍然可以得到正确的结果,只要在这段时间内没有溢出两次就行了。

0 comments on commit 36dd583

Please sign in to comment.