Raydium Swap —— Swap 指令详解

本文档详细拆解 Raydium CP-Swap 协议中交易(Swap)相关指令的代码实现。

1. 概述

Raydium CP-Swap 使用常量乘积(Constant Product) 模型进行代币交换:

1
x × y = k

其中 x 和 y 分别是两种代币的储备量,k 是常量。交易后 k 值只能增加(因为收取了手续费),不能减少。

2. Swap 账户结构(共用)

swap_base_inputswap_base_output 共用同一个账户结构。

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
// programs/cp-swap/src/instructions/swap_base_input.rs
use crate::curve::calculator::CurveCalculator;
use crate::error::ErrorCode;
use crate::states::*;
use crate::utils::token::*;
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};

#[derive(Accounts)]
pub struct Swap<'info> {
/// 执行交换的用户
pub payer: Signer<'info>,

/// 池子权限账户(PDA),用于签名转账
/// Seeds: ["vault_and_lp_mint_auth_seed"]
#[account(seeds = [crate::AUTH_SEED.as_bytes()], bump)]
pub authority: UncheckedAccount<'info>,

/// AMM 配置,读取费率参数
#[account(address = pool_state.load()?.amm_config)]
pub amm_config: Box<Account<'info, AmmConfig>>,

/// 池子状态
#[account(mut)]
pub pool_state: AccountLoader<'info, PoolState>,

/// 用户的输入代币账户
#[account(mut)]
pub input_token_account: Box<InterfaceAccount<'info, TokenAccount>>,

/// 用户的输出代币账户
#[account(mut)]
pub output_token_account: Box<InterfaceAccount<'info, TokenAccount>>,

/// 池子的输入代币储备(必须是 token_0_vault 或 token_1_vault)
#[account(
mut,
constraint = input_vault.key() == pool_state.load()?.token_0_vault
|| input_vault.key() == pool_state.load()?.token_1_vault
)]
pub input_vault: Box<InterfaceAccount<'info, TokenAccount>>,

/// 池子的输出代币储备(必须是 token_0_vault 或 token_1_vault)
#[account(
mut,
constraint = output_vault.key() == pool_state.load()?.token_0_vault
|| output_vault.key() == pool_state.load()?.token_1_vault
)]
pub output_vault: Box<InterfaceAccount<'info, TokenAccount>>,

/// 输入代币的 Token 程序
pub input_token_program: Interface<'info, TokenInterface>,

/// 输出代币的 Token 程序
pub output_token_program: Interface<'info, TokenInterface>,

/// 输入代币的 Mint
#[account(address = input_vault.mint)]
pub input_token_mint: Box<InterfaceAccount<'info, Mint>>,

/// 输出代币的 Mint
#[account(address = output_vault.mint)]
pub output_token_mint: Box<InterfaceAccount<'info, Mint>>,

/// 价格预言机状态
#[account(mut, address = pool_state.load()?.observation_key)]
pub observation_state: AccountLoader<'info, ObservationState>,
}

3. swap_base_input

基于固定输入数量的代币交换。用户指定输入数量,计算输出数量。

3.1 指令参数

参数 类型 说明
amount_in u64 输入代币数量
minimum_amount_out u64 最少输出代币数量(滑点保护)

3.2 指令逻辑

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
// programs/cp-swap/src/instructions/swap_base_input.rs
pub fn swap_base_input(
ctx: Context<Swap>,
amount_in: u64, // 输入代币数量
minimum_amount_out: u64, // 最少输出代币数量(滑点保护)
) -> Result<()> {
let block_timestamp = solana_program::clock::Clock::get()?.unix_timestamp as u64;
let pool_state = &mut ctx.accounts.pool_state.load_mut()?;

// 1. 检查池子是否允许交易且已到开放时间
if !pool_state.get_status_by_bit(PoolStatusBitIndex::Swap)
|| block_timestamp < pool_state.open_time
{
return err!(ErrorCode::NotApproved);
}

// 2. 计算输入代币的转账费用(Token-2022 支持)
let transfer_fee = get_transfer_fee(&ctx.accounts.input_token_mint, amount_in)?;
let actual_amount_in = amount_in.saturating_sub(transfer_fee);
require_gt!(actual_amount_in, 0);

// 3. 获取交易参数(交易方向、储备量、价格等)
let SwapParams {
trade_direction, // ZeroForOne 或 OneForZero
total_input_token_amount, // 输入代币储备(不含费用)
total_output_token_amount, // 输出代币储备(不含费用)
token_0_price_x64,
token_1_price_x64,
is_creator_fee_on_input, // 创建者费用是否从输入代币扣除
} = pool_state.get_swap_params(/* ... */)?;

// 4. 记录交换前的常量乘积
let constant_before = u128::from(total_input_token_amount)
.checked_mul(u128::from(total_output_token_amount))
.unwrap();

// 5. 调用曲线计算器计算交换结果
let creator_fee_rate = pool_state.adjust_creator_fee_rate(amm_config.creator_fee_rate);
let result = CurveCalculator::swap_base_input(
u128::from(actual_amount_in),
u128::from(total_input_token_amount),
u128::from(total_output_token_amount),
amm_config.trade_fee_rate,
creator_fee_rate,
amm_config.protocol_fee_rate,
amm_config.fund_fee_rate,
is_creator_fee_on_input,
).ok_or(ErrorCode::ZeroTradingTokens)?;

// 6. 验证常量乘积不减少(k 只能增加)
let constant_after = u128::from(result.new_input_vault_amount)
.checked_mul(u128::from(result.new_output_vault_amount))
.unwrap();
require_gte!(constant_after, constant_before);

// 7. 计算输出代币的转账费用,检查滑点
let amount_out = u64::try_from(result.output_amount).unwrap();
let output_transfer_fee = get_transfer_fee(&output_token_mint, amount_out)?;
let amount_received = amount_out.checked_sub(output_transfer_fee).unwrap();
require_gte!(amount_received, minimum_amount_out, ErrorCode::ExceededSlippage);

// 8. 更新累积费用
pool_state.update_fees(
u64::try_from(result.protocol_fee).unwrap(),
u64::try_from(result.fund_fee).unwrap(),
u64::try_from(result.creator_fee).unwrap(),
trade_direction,
)?;

// 9. 从用户账户转入输入代币到 vault
transfer_from_user_to_pool_vault(
ctx.accounts.payer.to_account_info(),
ctx.accounts.input_token_account.to_account_info(),
ctx.accounts.input_vault.to_account_info(),
ctx.accounts.input_token_mint.to_account_info(),
ctx.accounts.input_token_program.to_account_info(),
amount_in, // 原始输入量(含转账费)
ctx.accounts.input_token_mint.decimals,
)?;

// 10. 从 vault 转出输出代币到用户账户
transfer_from_pool_vault_to_user(
ctx.accounts.authority.to_account_info(),
ctx.accounts.output_vault.to_account_info(),
ctx.accounts.output_token_account.to_account_info(),
ctx.accounts.output_token_mint.to_account_info(),
ctx.accounts.output_token_program.to_account_info(),
amount_out,
ctx.accounts.output_token_mint.decimals,
&[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]],
)?;

// 11. 更新价格预言机
ctx.accounts.observation_state.load_mut()?.update(
oracle::block_timestamp(),
token_0_price_x64,
token_1_price_x64,
);
pool_state.recent_epoch = Clock::get()?.epoch;

Ok(())
}

4. swap_base_output

基于固定输出数量的代币交换。用户指定期望的输出数量,计算所需的输入数量。

4.1 指令参数

参数 类型 说明
max_amount_in u64 最大输入代币数量(滑点保护)
amount_out_received u64 期望接收的输出代币数量(不含转账费)

4.2 指令逻辑

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
// programs/cp-swap/src/instructions/swap_base_output.rs
use super::swap_base_input::Swap; // 复用 Swap 账户结构

pub fn swap_base_output(
ctx: Context<Swap>,
max_amount_in: u64, // 最大输入代币数量(滑点保护)
amount_out_received: u64, // 期望接收的输出代币数量
) -> Result<()> {
require_gt!(amount_out_received, 0);
let block_timestamp = solana_program::clock::Clock::get()?.unix_timestamp as u64;
let pool_state = &mut ctx.accounts.pool_state.load_mut()?;

// 1. 检查池子是否允许交易且已到开放时间
if !pool_state.get_status_by_bit(PoolStatusBitIndex::Swap)
|| block_timestamp < pool_state.open_time
{
return err!(ErrorCode::NotApproved);
}

// 2. 计算实际需要的输出量(含转账费)
let out_transfer_fee = get_transfer_inverse_fee(&output_token_mint, amount_out_received)?;
let amount_out_with_transfer_fee = amount_out_received.checked_add(out_transfer_fee).unwrap();

// 3. 获取交易参数
let SwapParams {
trade_direction,
total_input_token_amount,
total_output_token_amount,
token_0_price_x64,
token_1_price_x64,
is_creator_fee_on_input,
} = pool_state.get_swap_params(/* ... */)?;

// 4. 记录交换前的常量乘积
let constant_before = u128::from(total_input_token_amount)
.checked_mul(u128::from(total_output_token_amount))
.unwrap();

// 5. 调用曲线计算器计算所需输入量
let creator_fee_rate = pool_state.adjust_creator_fee_rate(amm_config.creator_fee_rate);
let result = CurveCalculator::swap_base_output(
u128::from(amount_out_with_transfer_fee),
u128::from(total_input_token_amount),
u128::from(total_output_token_amount),
amm_config.trade_fee_rate,
creator_fee_rate,
amm_config.protocol_fee_rate,
amm_config.fund_fee_rate,
is_creator_fee_on_input,
).ok_or(ErrorCode::ZeroTradingTokens)?;

// 6. 验证常量乘积不减少
let constant_after = u128::from(result.new_input_vault_amount)
.checked_mul(u128::from(result.new_output_vault_amount))
.unwrap();
require_gte!(constant_after, constant_before);

// 7. 计算实际需要的输入量(含转账费),检查滑点
let input_amount = u64::try_from(result.input_amount).unwrap();
let input_transfer_fee = get_transfer_inverse_fee(&input_token_mint, input_amount)?;
let input_transfer_amount = input_amount.checked_add(input_transfer_fee).unwrap();
require_gte!(max_amount_in, input_transfer_amount, ErrorCode::ExceededSlippage);

// 8. 更新累积费用
pool_state.update_fees(
u64::try_from(result.protocol_fee).unwrap(),
u64::try_from(result.fund_fee).unwrap(),
u64::try_from(result.creator_fee).unwrap(),
trade_direction,
)?;

// 9. 从用户账户转入输入代币到 vault
transfer_from_user_to_pool_vault(
ctx.accounts.payer.to_account_info(),
ctx.accounts.input_token_account.to_account_info(),
ctx.accounts.input_vault.to_account_info(),
ctx.accounts.input_token_mint.to_account_info(),
ctx.accounts.input_token_program.to_account_info(),
input_transfer_amount, // 实际输入量(含转账费)
ctx.accounts.input_token_mint.decimals,
)?;

// 10. 从 vault 转出输出代币到用户账户
transfer_from_pool_vault_to_user(
ctx.accounts.authority.to_account_info(),
ctx.accounts.output_vault.to_account_info(),
ctx.accounts.output_token_account.to_account_info(),
ctx.accounts.output_token_mint.to_account_info(),
ctx.accounts.output_token_program.to_account_info(),
amount_out_with_transfer_fee,
ctx.accounts.output_token_mint.decimals,
&[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]],
)?;

// 11. 更新价格预言机
ctx.accounts.observation_state.load_mut()?.update(
oracle::block_timestamp(),
token_0_price_x64,
token_1_price_x64,
);
pool_state.recent_epoch = Clock::get()?.epoch;

Ok(())
}

5. 交易方向

1
2
3
4
pub enum TradeDirection {
ZeroForOne, // 用 token_0 换 token_1
OneForZero, // 用 token_1 换 token_0
}

交易方向由 input_vaultoutput_vault 的地址决定:

  • 如果 input_vault == token_0_vault,则方向为 ZeroForOne
  • 如果 input_vault == token_1_vault,则方向为 OneForZero

6. 费用计算

6.1 费用类型

费用类型 说明 接收者
trade_fee 交易手续费 留在池子中(增加 LP 价值)
protocol_fee 协议费用 protocol_owner(从 trade_fee 中分配)
fund_fee 基金费用 fund_owner(从 trade_fee 中分配)
creator_fee 创建者费用 pool_creator(独立计算)

6.2 费用计算公式

1
2
3
4
5
总费用 = 输入量 × (trade_fee_rate + creator_fee_rate)
protocol_fee = 总费用 × trade_fee_rate × protocol_fee_rate
fund_fee = 总费用 × trade_fee_rate × fund_fee_rate
creator_fee = 总费用 × creator_fee_rate
净输入量 = 输入量 - 总费用

6.3 费用累积

费用会累积在 PoolState 中:

1
2
3
pool_state.protocol_fees_token_0 / protocol_fees_token_1
pool_state.fund_fees_token_0 / fund_fees_token_1
pool_state.creator_fees_token_0 / creator_fees_token_1

管理员可通过 collect_protocol_feecollect_fund_fee 提取,创建者可通过 collect_creator_fee 提取。

7. 常量乘积验证

每次交易后都会验证常量乘积不减少:

1
2
3
let constant_before = input_reserve × output_reserve;
let constant_after = new_input_reserve × new_output_reserve;
require_gte!(constant_after, constant_before);

由于收取了手续费,constant_after 通常会略大于 constant_before

8. Token-2022 支持

协议完整支持 Token-2022 的转账费用扩展:

  • swap_base_input: 输入量需要减去转账费用得到实际输入量
  • swap_base_output: 需要反向计算实际需要的输入量(含转账费)
1
2
3
4
5
// 正向计算转账费用
get_transfer_fee(mint, amount) -> fee

// 反向计算:给定期望接收量,计算需要发送的金额
get_transfer_inverse_fee(mint, receive_amount) -> fee

9. 总结

9.1 指令对比

特性 swap_base_input swap_base_output
固定参数 输入数量 输出数量
计算结果 输出数量 输入数量
滑点保护 minimum_amount_out max_amount_in
适用场景 “我要卖 X 个代币” “我要买 Y 个代币”

9.2 交易流程

1
2
3
4
5
6
7
8
9
1. 检查池子状态和开放时间
2. 计算转账费用
3. 获取交易参数(方向、储备量、价格)
4. 调用曲线计算器计算交换结果
5. 验证常量乘积不减少
6. 检查滑点保护
7. 更新累积费用
8. 执行代币转账
9. 更新价格预言机

9.3 价格预言机

每次交易后会更新价格预言机状态,记录交易前的价格:

1
2
3
4
5
observation_state.update(
block_timestamp,
token_0_price_x64, // token_1 / token_0 的价格(Q32 格式)
token_1_price_x64, // token_0 / token_1 的价格(Q32 格式)
);