跳转到主要内容

Documentation Index

Fetch the complete documentation index at: https://docs.raydium.io/llms.txt

Use this file to discover all available pages before exploring further.

本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →

手续费档位

CLMM 池在创建时会绑定一个 AmmConfig,该配置决定交易手续费率、协议份额和基金份额,以及价格刻度间距(参见 products/clmm/ticks-and-positions)。以下是常见的公开档位(以实时 GET https://api-v3.raydium.io/main/clmm-config 为准):
AmmConfig 索引trade_fee_rate刻度间距典型用途
0100(0.01%)1稳定币对
1500(0.05%)10相关性强的蓝筹币对
22_500(0.25%)60标准币对
310_000(1.00%)120高波动或长尾币对
交易手续费率的单位是 1/FEE_RATE_DENOMINATOR = 1/1_000_000 的交易量。协议费率和基金费率使用相同的分母,但作用于交易手续费而非交易量——这与 CPMM 的约定一致。

每笔 swap 的手续费拆分

在 swap 的每个步骤中(参见 products/clmm/math):
step_trade_fee   = ceil(step_input * trade_fee_rate / 1_000_000)
step_protocol    = floor(step_trade_fee * protocol_fee_rate / 1_000_000)
step_fund        = floor(step_trade_fee * fund_fee_rate     / 1_000_000)
step_lp          = step_trade_fee - step_protocol - step_fund
  • step_lp 流入 fee_growth_global_{input_side}_x64,并按当前活跃流动性缩放:fee_growth_global += step_lp × 2^64 / pool.liquidity
  • step_protocol 计入 PoolState.protocol_fees_token_{input_side},通过 CollectProtocolFee 提取。
  • step_fund 计入 PoolState.fund_fees_token_{input_side},通过 CollectFundFee 提取。
与 CPMM 相同,协议费和基金费虽存放在金库中,但不计入曲线的流动性视图:swap 数学计算读取的是 pool.liquidity,该值不受待提取手续费的影响。

手续费按代币方向分配的原因

在 CPMM 中,swap 手续费始终从输入代币扣除,另一侧不会产生协议/基金应计费。CLMM 在每个步骤中遵循相同规则:手续费计入该步骤的输入代币。由于多个刻度的 swap 方向一致,所有步骤均从同一代币扣费——因此实际上,任意一笔 swap 的手续费只流向某一侧 若用户将 token0 换成 token1,则 fee_growth_global_0_x64 上升,fee_growth_global_1_x64 不变。本次 swap 中头寸以 token0 获得手续费。下一笔 swap 可能方向相反,转而增加 fee_growth_global_1_x64。随着时间推移,均衡的池子两侧都会积累手续费。

单侧手续费(CollectFeeOn

通过 CreateCustomizablePool 创建的池子可选择非默认的手续费收取模式。该模式在池子创建时固定,存储在 PoolState.fee_on 中。
CollectFeeOnfee_on 字节行为
FromInput(默认)0经典 Uniswap-V3 模式——手续费始终从每个 swap 步骤的输入代币扣除,输入代币随 swap 方向交替。
Token0Only1手续费始终以 token0 计价。对于 0→1 的 swap,手续费来自输入代币(与 FromInput 相同);对于 1→0 的 swap,手续费从 swap 输出(token0)扣除。
Token1Only2Token0Only 对称——手续费始终以 token1 计价。
为什么池子会选择 Token0OnlyToken1Only——目的是让 LP 获得单一、可预期的计价货币。对于 MEMECOIN / USDC 这类以美元计价的 LP,选择 Token1Only(手续费始终结算为 USDC)后,LP 的盈亏不再受交易方向占比的影响。代价是:当手续费从 swap 输出扣除时,用户收到的是 out − fee 而非从输入端扣除的近似原值,因此报价逻辑须从输出端减去手续费。SDK 的 computeAmountOut 已根据 fee_on 处理这一分支;直接读取 pool.fee_on 的客户端代码应参照 PoolState 上的辅助函数:
pool.is_fee_on_input(zero_for_one: bool) -> bool   // true → fee is deducted from input
pool.is_fee_on_token0(zero_for_one: bool) -> bool  // for telemetry / accounting
LP 层面的影响——手续费仍通过标准的 fee_growth_global_{0,1}_x64 累加器按 swap 步骤路由,因此头寸仍使用相同的 fee_growth_inside 公式结算手续费。不对称性仅体现在侧面应计方向,而非数学计算本身。 fee_on 在创建后不可修改。通过旧版 CreatePool 创建的池子永久为 FromInput 模式。

动态手续费

enable_dynamic_fee = true 创建的池子会在 AmmConfig.trade_fee_rate 基础上叠加一个基于波动率的附加费率。该机制是对 Trader Joe / Meteora 动态手续费设计的简化移植。

状态

PoolState.dynamic_fee_info 包含五个校准参数(池子创建时 DynamicFeeConfig 的快照)以及每次 swap 更新的四个状态字段。字节布局参见 products/clmm/accounts

每笔 swap 的更新

每个 swap 步骤程序依次执行三个子步骤:
  1. 衰减参考值。若 now - last_update_timestamp > filter_period,波动率参考值衰减:
    if elapsed > decay_period:
        volatility_reference = 0
    elif elapsed > filter_period:
        volatility_reference = volatility_accumulator * reduction_factor / 10_000
    # else: hold the previous reference
    
  2. 更新累加器。新的累加器等于参考值加上已遍历的绝对距离(以 tick_spacing 为单位)乘以粒度缩放系数,并不超过配置的最大值:
    delta_idx     = abs(tick_spacing_index_reference - current_tick_spacing_index)
    accumulator   = volatility_reference + delta_idx * 10_000   // VOLATILITY_ACCUMULATOR_SCALE
    accumulator   = min(accumulator, max_volatility_accumulator)
    
  3. 计算附加费率。附加费率关于累加器呈抛物线形(因为 swap 的”刻度距离”在标准公式中被平方),并通过 dynamic_fee_control 进行增益缩放:
    fee_increment_rate = dynamic_fee_control * (accumulator * tick_spacing)^2
                       / (100_000 * 10_000^2)
    fee_rate           = AmmConfig.trade_fee_rate + fee_increment_rate
    fee_rate           = min(fee_rate, 100_000)              // 10% cap
    
10% 上限(1e6 单位下 MAX_FEE_RATE_NUMERATOR = 100_000)为硬编码安全限制;在实践中,调优后的配置通常远低于此上限。

参数选择

以下是在试点池中有效的默认范围:
参数典型范围说明
filter_period30 – 60 秒在微波动期间保持参考值;值越小响应越灵敏
decay_period300 – 1800 秒平静窗口结束后,手续费回归基准
reduction_factor4_000 – 8_000基于 10_000,值越高则高位手续费越难回落
dynamic_fee_control1_000 – 50_000基于 100_000,曲线增益系数
max_volatility_accumulator100_000 – 10_000_000附加费率的饱和上限
建议将历史 swap 数据代入公式进行离线回放,再调整 dynamic_fee_control,使平均手续费符合目标(例如,1σ 行情日为基准的 1.5 倍,3σ 行情日为 5 倍)。

LP 所见

动态手续费收入与基础手续费流经相同的累加器——fee_growth_global_{0,1}_x64,不存在单独的”动态手续费增长”字段。高波动池中的 LP 在波动期间自然获得更高手续费,无需额外的领取或结算指令。

集成方须知

  • 即使池子储备未发生变化,某笔报价在区块 N 与区块 N+1 之间也可能发生变化——每笔 swap 都会改变波动率累加器。Trade API 的报价在报价区块时有效;若在报价与执行之间有响应型池子被触发,实际费率可能偏差数个 bps。
  • volatility_accumulatorlast_update_timestamp 是链上公开数据,客户端可在离线模拟中直接复现该公式。

每个头寸的手续费记账

每个头寸在最后一次被触碰时存储:
  • fee_growth_inside_0_last_x64fee_growth_inside_1_last_x64——该快照时刻的区间特定手续费增长值。
此后每次被触碰(IncreaseLiquidityDecreaseLiquidity,以及任何更新刻度边界手续费增长的状态转换)时:
  1. 程序从全局手续费增长和两个端点刻度的 fee_growth_outside_* 重新计算 fee_growth_inside_{0,1}_x64
  2. Δ 按头寸流动性权重累加至 tokens_fees_owed_{0,1}
    Δ_fee_growth_inside_0 = fee_growth_inside_now_0 - fee_growth_inside_last_0
    tokens_fees_owed_0  += Δ_fee_growth_inside_0 * position.liquidity / 2^64
    
  3. fee_growth_inside_{0,1}_last_x64 更新。
代币实际转移仅发生在 DecreaseLiquidity 或专用的 CollectFees 路径(在 Raydium 当前的指令集中,手续费作为 DecreaseLiquidity 的一部分被提取)。在 DecreaseLiquidity 调用中将 liquidity = 0 是”仅收集手续费”的标准写法。

区间外头寸不获得手续费

若头寸区间不包含 tick_current,则为其计算的 fee_growth_inside 受到上界约束,价格在区间外时不会增长。该头寸停止累积手续费,直到价格重新回到其区间。这是设计特性而非缺陷——正是集中流动性将手续费收益与资本同步集中的方式。

奖励流

一个 CLMM 池最多可同时运行三个奖励流。每个奖励流是一个(奖励 mint、发放速率、开始时间、结束时间)的元组,存储在 PoolState.reward_infos[i] 中。
pub struct RewardInfo {
    pub reward_state: u8,               // Uninitialized | Initialized | Open | Ended
    pub open_time: u64,
    pub end_time: u64,
    pub last_update_time: u64,
    pub emissions_per_second_x64: u128, // Q64.64 reward tokens per second
    pub reward_total_emissioned: u64,
    pub reward_claimed: u64,
    pub token_mint:    Pubkey,
    pub token_vault:   Pubkey,
    pub authority:     Pubkey,           // who can SetRewardParams / fund
    pub reward_growth_global_x64: u128,  // accumulator, Q64.64
}

结算循环

每条涉及流动性的指令(以及作为独立指令的 UpdateRewardInfos)都会将所有活跃奖励流推进到 now
for each reward_info with state in {Open, Ended within grace}:
    elapsed         = min(now, end_time) − last_update_time
    if elapsed > 0 && pool.liquidity > 0:
        reward_growth_global_x64 += emissions_per_second_x64 × elapsed × 2^64 / pool.liquidity
        reward_total_emissioned  += emissions_per_second × elapsed
    last_update_time = min(now, end_time)
若某段时间内 pool.liquidity == 0,该区间内的奖励发放将被跳过(无区间内流动性可分配)。剩余预算留在奖励金库中。协议方可通过 SetRewardParams 追加资金或结束奖励流。

每个头寸的奖励应计

与手续费完全一致,额外增加每个奖励流的维度:
for each stream i:
    reward_growth_inside_now_i   = compute_inside_i(pool, tick_lower, tick_upper)
    Δ_i = reward_growth_inside_now_i - personal_position.reward_infos[i].growth_inside_last_x64
    personal_position.reward_infos[i].reward_amount_owed += Δ_i * personal_position.liquidity / 2^64
    personal_position.reward_infos[i].growth_inside_last_x64 = reward_growth_inside_now_i
用户通过 CollectReward 领取奖励,该指令将 reward_amount_owed 从奖励流金库转至用户账户并将计数器清零。

只有区间内头寸获得奖励

reward_growth_insidefee_growth_inside 使用相同的公式——通过刻度外部累加器计算——因此当前价格区间外的头寸不会累积奖励。这与 Uniswap v3”激励归于活跃流动性”的设计理念一致,也将 LP 利益与现货价格覆盖范围对齐。

资金注入与结束奖励流

奖励流通过 InitializeReward 创建,该指令会预先将总预算(emissions_per_second × (end_time − open_time))存入奖励金库。若资金方余额不足,程序会拒绝执行 InitializeRewardSetRewardParams 可延长 end_time 或提高发放速率;缩减上述任一参数的操作会被阻止,以防止对已承诺给 LP 的奖励进行撤销。 now > end_time 时,奖励流进入 Ended 状态,但其 reward_growth_global_x64 仍可被读取——LP 在发放结束很久后仍可通过 CollectReward 提取历史已赚取的奖励。

管理员收取手续费

签名方指令效果
amm_config.ownerCollectProtocolFeeprotocol_fees_token_{0,1} 转至指定接收方。
amm_config.fund_ownerCollectFundFeefund_fees_token_{0,1} 转至指定接收方。
两者均不影响曲线——已应计金额本就在 pool.liquidity 之外。关于主网上这些签名方的持有者,参见 security/admin-and-multisig

Token-2022 交互

手续费和奖励均以池子或奖励流中某一代币计价。Token-2022 扩展的行为与 CPMM 中相同:
  • swap 输入 mint 的转账费。 池子收到 amount_in − mint_transfer_fee。CLMM 程序的步骤输入按净额计算,因此池子的手续费累加器反映的是实际入库代币。
  • swap 输出 mint 的转账费。 池子发出 amount_out,用户收到 amount_out − mint_transfer_fee。滑点检查应基于用户实际收到的金额。
  • 奖励 mint 的转账费。InitializeReward 时,奖励以”入库”单位计价(资金方支付 mint 转账费存入金库)。在 CollectReward 时还会再次产生 mint 转账费;LP 应预计转账费奖励代币会有一定折扣。
  • 不可转让 / 机密 / 群组成员 mint。CreatePool / InitializeReward 时会被拒绝。
对于多跳含转账费的 swap,综合影响可能相当显著。忽略此影响的报价器会高估输出;参见 algorithms/token-2022-transfer-fees 了解参考计算方法。

链下读取手续费与奖励

const pool = await raydium.clmm.getPoolInfoFromRpc(poolId);
const position = await raydium.clmm.getOwnerPositionInfo({
  wallet: owner.publicKey,
});

for (const p of position) {
  console.log("Position", p.nftMint.toBase58(),
              "range", p.tickLower, "→", p.tickUpper,
              "L", p.liquidity.toString(),
              "fees owed:", p.tokenFeesOwed0.toString(),
              p.tokenFeesOwed1.toString(),
              "rewards owed:", p.rewardInfos.map(r => r.rewardAmountOwed.toString()));
}
tokenFeesOwed*rewardAmountOwed 是头寸最后一次被触碰时的快照。若要查看当前值(包含此后的增长),可以在模拟中以零流动性调用 IncreaseLiquidity,或直接使用全局 fee_growth_* 及两个刻度外部快照重新计算。

延伸阅读

参考资料: