本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →
本页与
products/clmm/accounts(账户是什么)和 products/clmm/math(数学是什么)配套。它是参数和账户顺序的权威参考;具体的字节布局来自 IDL。2026-09 程序升级在 Anchor 1.0.2 / Solana 3.1.10 上重建了 CLMM,添加了管理员指令 CollectExcessLamports,并改变了 CreateAmmConfig 写入 owner / fund_owner 的内容。没有用户面向的指令改变了其账户、参数或数学。参见 2026-09-30 更新日志条目。指令清单
没有初始化 tick 数组指令,也不需要。 Tick 数组在
OpenPosition* / IncreaseLiquidity* 内部由 TickArrayState::get_or_create_tick_array 创建,由 payer 支付。将(可能仍未初始化、系统拥有的)tick 数组 PDA 作为 tick_array_lower / tick_array_upper 传递,程序在缺失时分配它。OpenLimitOrder 对其单个 tick_array 执行相同操作。CreateAmmConfig、UpdateAmmConfig、UpdatePoolStatus、CreateOperationAccount、UpdateOperationAccount、CloseProtocolPosition)由程序的硬编码 admin 公钥门控。CreatePermissionPda / ClosePermissionPda 接受 admin 公钥或专用的 permission_pda_admin 密钥;CreateSupportMintAssociated / CloseSupportMintAssociated 接受 admin 公钥或专用的支持铸币所有者密钥;CollectExcessLamports 接受 admin 公钥或专用的清理 lamports 钱包。奖励流管理员指令(TransferRewardOwner、CollectRemainingRewards)由奖励资金提供者门控,而不是程序管理员。
V2 后缀表示”支持保管库/NFT 上的 Token-2022,需要位图扩展槽”。SDK 默认为新池选择 V2。
CreatePool
参数
前置条件
token_mint_0 < token_mint_1按字节顺序。amm_config.disable_create_pool == false。- 铸币不被 Token-2022 扩展允许列表拒绝。
pool_state.sqrt_price_x64 = sqrt_price_x64,tick_current = floor(log_{1.0001}(price))。pool_state.liquidity = 0(还没有头寸)。pool_state.fee_on = FromInput(旧版默认)。pool_state.dynamic_fee_info被清零(动态费用禁用)。
CreateCustomizablePool
推荐用于新池。与 CreatePool 相同的效果加上按池费用收集模式和可选的动态费用选择加入。
参数
CreatePool 的 13 个声明账户。dynamic_fee_config 不是声明账户。
前置条件 — 与 CreatePool 相同。如果 enable_dynamic_fee = false,不需要 remaining_account,任何传入的都仅被扫描以查找支持铸币记录。
后置条件
pool_state.fee_on设置为选定的CollectFeeOn变体。- 如果启用了动态费用:
pool_state.dynamic_fee_info从提供的DynamicFeeConfig初始化(复制五个校准参数;状态字段清零)。 - 否则:
pool_state.dynamic_fee_info被清零(= 此池永远禁用动态费用)。
fee_on 和动态费用启用位仅在池创建时设置。没有原地升级 — 通过旧版 CreatePool 创建的池无法追溯获得动态费用或单边费用。新部署应默认使用此指令。
CreatePermissionedPool
CreatePool 和 CreateCustomizablePool 都从 ["pool", amm_config, token_mint_0, token_mint_1] 派生池 PDA,因此每个 (config, mint0, mint1) 三元组恰好有一个规范池地址 — 在相同种子处的第二个 init 失败。CreatePermissionedPool 通过将客户端提供的 seed_index: u16 折叠到池 PDA 种子中来解除该限制,允许同一对和费用等级的多个池 — 每个在其自己的地址处。因为任意池地址是特权能力,支付者必须持有授权它的 Permission PDA。
关于池的其他一切与 CreateCustomizablePool 相同:它采用相同的 CreateCustomizableParams 并支持单边费用和动态费用选择加入。
参数
CreateCustomizablePool 相同加上,在前面:
pool_state PDA 从 ["pool", amm_config, token_mint_0, token_mint_1, seed_index.to_le_bytes()] 派生。
前置条件
seed_index != 0。seed_index为0保留给旧版池并在此处被拒绝;[0, 0]种子分量是使旧版池地址折叠到经典四种子形式的原因。payer的permissionPDA 存在(由管理员通过CreatePermissionPda创建)。- 与
CreatePool相同的铸币/允许列表规则。
- 新的
pool_state存在于seed_index派生的地址处,pool_state.seed_index = seed_index。 - 所有其他后状态与
CreateCustomizablePool匹配(费用模式、可选动态费用)。
此指令不会扩大一般池创建访问 — 无权限创建继续通过
CreatePool / CreateCustomizablePool,它们仍然是每对一个池。CreatePermissionedPool 存在于特定情况,其中白名单操作者需要同一对的多个池(例如不同的初始价格或启动队列)并持有由管理员授予的 Permission PDA。OpenPositionV2 / OpenPositionWithToken22Nft
在现有池内创建新头寸。
参数
OpenPositionV2 声明 22;OpenPositionWithToken22Nft 声明 20。
OpenPositionV2,按顺序:
OpenPositionWithToken22Nft 是相同的列表,其中 metadata_account(5)和 metadata_program(19)被移除 — 它通过 NFT 铸币上的 Token-2022 元数据扩展写入头寸的元数据 — 给出 20 个账户。其 position_nft_mint 是裸 Signer,position_nft_account 是 UncheckedAccount。
数学 — 参见 products/clmm/math。给定 base_flag,程序将 liquidity 或 (amount_0_max, amount_1_max) 解析为实际 L 和消耗的实际代币金额。
前置条件
tick_lower < tick_upper,都是pool.tick_spacing的倍数,在[MIN_TICK, MAX_TICK]内。- 传递两个 tick 数组 PDA。它们不需要已经存在 —
get_or_create_tick_array在此指令内以payer的费用分配缺失的。没有单独的初始化 tick 数组指令。 - 用户在源 ATA 中至少有
amount_0_max和amount_1_max。
personal_position存在,liquidity设置,fee_growth_inside_last快照。tick_lower和tick_upper处的 Tick 数组条目更新(liquidity_gross += L,liquidity_net ± L,维护费用增长快照)。- 如果头寸在范围内(
tick_lower ≤ tick_current < tick_upper),pool_state.liquidity += L。 - 头寸 NFT 铸币记录
pool_state作为冻结权限。铸币权限在铸造单个 NFT 后被移除。记录冻结权限不改变 NFT 代币账户的状态。 - NFT 代币账户保持解冻,除非指令是
OpenPositionV2或OpenPositionWithToken22Nft且任一保管库铸币的冻结权限与 CLMM 的受限发行者列表匹配。仅该匹配的 V2 路径冻结账户。OpenPositionV1 不冻结。
TickInvalidOrder(tick_lower >= tick_upper)、TickAndSpacingNotMatch(端点不是 tick_spacing 的倍数)、InvalidTickIndex(在 [MIN_TICK, MAX_TICK] 之外)、MissingTickArrayBitmapExtensionAccount(范围在内联位图之外且未追加扩展)、NotApproved(pool_state.status 阻止打开)、ZeroAmountSpecified。
头寸冻结不添加声明的指令账户或参数。客户端可以使用现有的 V2 布局打开这些头寸。行为在链上从
vault_0_mint 和 vault_1_mint 选择。IncreaseLiquidityV2
向已打开的头寸添加流动性。
参数
OpenPosition 派生:没有 rent、没有 system_program、没有 associated_token_program 和没有元数据账户。
当头寸的范围在内联位图之外时,将
TickArrayBitmapExtension PDA 作为 remaining_accounts[0] 前置。
效果
- 转移
amount_0_actual/amount_1_actual从用户 → 保管库。 - 增加
personal_position.liquidity和pool_state.liquidity(如果在范围内),以及端点 tick 的liquidity_gross/liquidity_net相应地。 - 收集自上次接触以来应付的费用和奖励并将其记入
token_fees_owed_{0,1}/reward_amount_owed。这些仅在DecreaseLiquidity/DecreaseLiquidityV2上支付,而不是在增加时 — 没有独立的收集指令。
DecreaseLiquidityV2
从头寸移除流动性。
参数
IncreaseLiquidityV2 的相同形状或顺序:personal_position 和 pool_state 被交换,保管库在 tick 数组之前,用户端账户被命名为 recipient_token_account_*,还有额外的 memo_program。
剩余账户 — 每个被收集的活跃奖励三个,按顺序
reward_token_vault(W)、recipient_token_account(W)、reward_vault_mint。当头寸的范围在内联位图之外时,前置 TickArrayBitmapExtension PDA。
这也是唯一收集费用和奖励的方式。要在不改变头寸的情况下收集,使用
liquidity = 0、amount_0_min = 0、amount_1_min = 0 调用它。- 给定当前
sqrt_price_x64,计算移除的L的(amount_0, amount_1)。 - 结算自上次接触以来累积的费用/奖励,与
IncreaseLiquidity相同。 - 从保管库转移
amount_0 + fees_owed_0和amount_1 + fees_owed_1到用户。 - 递减流动性计数器;如果新的
personal_position.liquidity == 0,头寸符合ClosePosition的条件。
amount_0_min 和 amount_1_min 是用户接受的最小值,扣除输出端 Token-2022 转移费用。
ClosePosition
销毁头寸 NFT 并关闭 PersonalPositionState。
声明账户
剩余账户
- 解冻 NFT:不需要;额外的池账户是无害的,因为处理程序不读取它。
- 冻结 NFT:将
personal_position.pool_id作为第一个剩余账户追加。程序将其加载为PoolState并使用其 PDA 种子签署解冻。
personal_position.liquidity == 0。tokens_fees_owed_{0,1} == 0。- 所有奖励计数器
reward_amount_owed == 0。
- 如果 NFT 代币账户被冻结,验证第一个剩余账户等于
personal_position.pool_id,然后使用池 PDA 解冻它。 - 销毁 NFT。
- 关闭 NFT 代币账户和
personal_position,将租金退款给nft_owner。如果头寸 NFT 使用 Token-2022,它也关闭 NFT 铸币;经典 SPL Token 铸币无法关闭并保持供应为零。
AccountLack。为每个关闭传递池是最简单的兼容策略。
SwapV2
沿流动性曲线行走;精确输入或精确输出取决于 is_base_input。
参数
调用者传递覆盖预期交换行走的排名 tick 数组列表;程序使用所需的数量。SDK 通过
PoolUtils.computeAmountOutFormat 或 API 的报价端点计算此列表。
前置条件
pool_state.status允许交换。now >= open_time。sqrt_price_limit_x64在sqrt_price_x64的正确一侧用于方向。
TooLittleOutputReceived(精确输入滑点)、TooMuchInputPaid(精确输出滑点)、SqrtPriceLimitOverflow、NotEnoughTickArrayAccount、InvalidFirstTickArrayAccount、MissingTickArrayBitmapExtensionAccount、LiquidityInsufficient、NotApproved(在 pool_state.status 上设置交换位)。CLMM 没有 ExceededSlippage 变体 — 那个名称是 CPMM 的 — 也没有 TickArrayNotFound。
SwapV2 在内部做什么调用者应该知道的(2025 年后发布):
- 动态费用附加费 — 如果
pool.dynamic_fee_info非零,程序使用自上次交换以来遍历的 tick 距离更新波动率累加器(使用来自products/clmm/fees的过滤/衰减规则)并在AmmConfig.trade_fee_rate之上添加dynamic_fee_component。总费用上限为 10%(MAX_FEE_RATE_NUMERATOR / 1_000_000)。 - 限价单匹配 — 当价格行走穿过持有开放限价单的 tick 时,程序首先在该 tick 处填充可用的限价单流动性(按
order_phase的 FIFO),然后沿 LP 流动性曲线进行。已成交金额更新tick.unfilled_ratio_x64和tick.part_filled_orders_remaining以供后续结算;订单本身保持未支出,直到其所有者调用SettleLimitOrder。 - 单边费用路由 — 当
pool.fee_on = Token0Only或Token1Only时,交换步骤仍计算相同的输入输出交易;费用然后被路由到配置的一侧。对于配置的费用侧是输出的方向,费用从交换输出中扣除(用户接收out − fee);对于配置的费用侧是输入的方向,行为与FromInput匹配。参见PoolState上的is_fee_on_input(zero_for_one)和is_fee_on_token0(zero_for_one)。
Swap(V1)实现与 SwapV2 相同的动态费用、单边费用路由和限价单匹配;它缺少的唯一功能是 Token-2022 支持 — 两个保管库都必须是经典 SPL Token。任何 Token-2022 铸币的池必须通过 SwapV2 交换。聚合器和 SDK 已经为每个 CLMM 腿优先选择 V2,因此调用者不必根据铸币类型分支。
OpenLimitOrder
在特定 tick 处下达卖单。订单坐在按 tick 的 FIFO 队列中,并在价格穿过时填充。
参数
剩余账户 —
[0] tick_array_bitmap_extension,仅当初始化其起始索引落在池的内联位图之外的 tick 数组时需要。否则不传递任何内容。
账户列表变化(2026-07 发布)。
OpenLimitOrder 现在也采用输出侧账户 — output_token_account、output_vault 和 output_vault_mint — 除了输入侧。它们仅用于验证:如果所有者的输入或输出代币账户被冻结,程序拒绝订单。这保证填充实际上可以结算到所有者的输出 ATA,这对于允许列表/默认冻结 Token-2022 铸币(例如许可代币)很重要,其中账户可能尚未解冻。针对较旧的单侧账户列表构建的客户端必须添加三个输出账户。input_token_account和output_token_account都未被冻结(否则NotApproved)。pool_state.status允许交换(位 4)和限价单(位 5)操作(否则NotApproved)。tick_index % pool.tick_spacing == 0并在[MIN_TICK, MAX_TICK]内。tick_index在选定方向的pool.tick_current的右侧 — 卖 token0 → tick 必须在当前之上,反之亦然。在已穿过的 tick 处卖出会立即被匹配并被拒绝。
limit_order存在,快照打开时的tick.order_phase和tick.unfilled_ratio_x64。tick.orders_amount += amount(在当前队列中)。limit_order_nonce.order_nonce += 1。- 发出
OpenLimitOrderEvent。
NotApproved(输入或输出代币账户冻结,或池禁用交换/限价单)、ZeroAmountSpecified(输入侧转移费用后 amount == 0)、InvalidLimitOrderAmount(金额会在该 tick 处产生低于 1 个基本单位的输出,或溢出 u64)、InvalidTickIndex(在 [MIN_TICK, MAX_TICK] 之外,或在选定方向的 tick_current 的错误一侧)、TickAndSpacingNotMatch(tick_index % pool.tick_spacing != 0)、OrderPhaseSaturated。
IncreaseLimitOrder
添加到现有开放订单。仅由订单的 owner 调用。
参数
system_program。
前置条件
limit_order.owner == signer。- 订单仍在同一队列中(
tick.order_phase == limit_order.order_phase)。如果队列已开始填充,订单部分结算 — 调用者应首先调用DecreaseLimitOrder或SettleLimitOrder以前进。
- 从所有者 ATA 转移
amount到input_vault。 limit_order.total_amount += amount;tick.orders_amount += amount。
DecreaseLimitOrder
减少或完全取消开放订单。将未成交的剩余部分支付回所有者,加上任何已由过去部分填充结算的输出。
参数
效果
- 从打开以来的队列的
unfilled_ratio_x64重新计算订单的已成交金额。 - 将已成交输出发送到
output_token_account。 - 将
amount的未成交输入发送回input_token_account。 - 相应地更新
limit_order。如果新的未成交剩余为零,程序关闭账户并将租金退款给owner。
SettleLimitOrder
将已成交的输出代币推送给所有者,而不改变订单的未成交剩余。当 auto_withdraw 保管人想要逐步支付长期运行的部分填充时很有用。
调用者 — 订单的 owner,或程序的 limit_order_admin(运行自动保管人循环的离线操作热钱包)。保管人没有其他权限 — 它无法将用户资金移出将已成交输出推送到订单的 owner ATA 之外。
账户
效果
- 使用
(limit_order.unfilled_ratio_x64, tick.unfilled_ratio_x64)计算累积应付输出。 - 将增量转移到
output_token_account。 - 更新
limit_order.settled_output。 - 不关闭订单;它仍对任何剩余输入开放。
CloseLimitOrder
关闭完全消耗的订单账户。租金始终返回给 limit_order.owner,无论谁签署。
调用者 — owner 或 limit_order_admin。
前置条件
- 订单有零未成交剩余(要么
amount == total_amount被填充和结算,要么所有者之前将订单减少到零并忘记关闭)。
- 关闭
limit_order;租金被发送到limit_order.owner。
CreateDynamicFeeConfig(管理员)
在 u16 索引下创建可重用的参数集。
参数
常见错误 — 如果
decay_period <= filter_period 或任何 0 值字段超出范围,则 InvalidDynamicFeeConfigParams。
UpdateDynamicFeeConfig(管理员)
修改现有的 DynamicFeeConfig。已在创建时快照配置的池不会被追溯更新;仅引用此配置的新创建池将获取新值。
参数 — 与 CreateDynamicFeeConfig 相同的五个校准字段(filter_period、decay_period、reduction_factor、dynamic_fee_control、max_volatility_accumulator);index 在创建时固定,此处不重新传递。
CollectProtocolFee / CollectFundFee
从池的保管库清理累积的协议/基金费用到收件人,将相应的 PoolState.protocol_fees_* / fund_fees_* 字段清零。这不是 CPMM 的布局 — CLMM 没有 authority 账户,收件人字段被命名为 recipient_token_account_{0,1} 而不是 recipient_token_{0,1}_account。
参数 — amount_0_requested: u64、amount_1_requested: u64。
在 2026-09 中更改:新配置不再从签署者获取其费用所有者。
create_amm_config 现在将程序的硬编码 protocol_fee_owner::ID 写入 owner 并将 fund_fee_owner::ID 写入 fund_owner。之前,它将管理员签署者的密钥复制到两者。在主网上,这些已经是存储在所有 21 个现有配置上的相同密钥。地址在 reference/program-addresses 中。上面接受的签署者未改变。管理员仍可以从任何配置收集,现有的 AmmConfig 账户不被重写。从账户读取 owner / fund_owner 而不是假设任一值。UpdateAmmConfig 参数 3 和 4 仍然轮换它们。CollectExcessLamports
管理员清理 CLMM 控制的账户上超过租金豁免最低限额的 lamports。它在 2026-09 升级中添加,以回收 SIMD-0437 租金减少 在每个步骤之前创建的账户上留下的过度资金。
仅多余部分移动。代币余额、账户数据、所有者、池状态和价格曲线保持不变。已在其最低限额的账户保持原样,因此指令可以在每个推出步骤后安全地重新运行。
参数:无。
账户
范围:每次调用一个池。 CLMM 没有程序范围的保管库权限。每个池的保管库由其自己的
PoolState 拥有。每个代币程序源必须因此在槽 2 处有 pool_state 作为其权限:token_vault_0、token_vault_1 或该池的奖励保管库之一。如果你传递另一个池的保管库或用户的代币账户,代币程序的所有者检查失败,整个指令恢复。它不被跳过。头寸 NFT 铸币无法被清理,因为当头寸打开时其铸币权限被撤销。
每个源账户如何处理
程序对 remaining_accounts 进行两次传递:首先是每个代币程序 CPI,然后是直接借记。交错两者会中止运行时的 UnbalancedInstruction 错误,因此顺序在程序中是固定的,你不必自己排序列表。

