Skip to main content
Use CPI when your Solana program has to call DLMM Pro itself, for example to swap as part of a larger instruction, to hold a position or a limit order in a program-controlled account, or to claim fees and rewards on behalf of a vault. The integration contract is the deployed program ID, the published IDL and the TypeScript SDK. Take account lists from the IDL and compare them with an SDK-built instruction before you ship.
DLMM Pro is in beta and is subject to breaking changes. Check the changelog before you upgrade the IDL you generate bindings from.

Program ID

The pool authority is a PDA of the seed pool_auth under the program. It owns every reserve and reward vault and signs the transfers out of them, so it is the transfer authority a transfer hook sees for tokens leaving the pool.
declare_program! also generates dlmm_pro::ID, taken from the address field of the IDL, so you can use it in place of the first constant.

Generated Bindings

Generate bindings from the IDL with Anchor’s declare_program!. Do not type account orders from memory. A misplaced optional account, event account or tick array is the usual cause of a failing CPI.

Getting the IDL

The IDL ships with the SDK. It is the same file in each of these places: The file must be named dlmm_pro.json, because declare_program!(dlmm_pro) reads idls/dlmm_pro.json. The macro looks for an idls directory in the crate directory and then in each parent directory, and uses the nearest one. In a workspace, a single idls directory at the workspace root works for every crate.
The -f flag makes curl fail instead of saving an error page as the IDL. You can also copy the file from the npm package: node_modules/@meteora-ag/dlmm-pro-sdk/dist/idl/dlmm-pro/dlmm_pro.json holds the same IDL. Take the IDL from the SDK release you pin, and update it when you upgrade the SDK. A new program release changes the IDL.

Cargo and Anchor

The program in this page is built with Anchor 1.x. declare_program! generates code that uses bytemuck, so the crate needs it as a direct dependency.
The [features] entries declare the feature names that Anchor’s macros test, and the [lints] entry registers the cfg names they use, so the crate builds without unknown-cfg warnings. The examples compile with Anchor 1.0.2, 1.1.2 and 1.2.2. Keep anchor-lang, anchor-spl and the Anchor macro crates on the same version. Anchor 1.1 and newer need Rust 1.89, so build with cargo build-sbf from a Solana release whose platform tools ship Rust 1.89 or newer (v1.52 with Agave 3.1, v1.54 with Agave 4.1). CpiContext::new takes the program ID as a Pubkey in Anchor 1.x. In Anchor 0.31 it takes the program AccountInfo, so the code on this page does not build against 0.x.

What the Macro Generates

declare_program!(dlmm_pro) creates a module dlmm_pro in your crate: The account types in dlmm_pro::accounts describe only the fixed header. Pool accounts with extensions and every TickArray, PositionArray, LimitOrder and Oracle have a longer, variable tail. Anchor’s Account<'info, dlmm_pro::accounts::Pool> reads the whole account as the header and panics on a pool with a tail. To read a pool in your program, use AccountLoader<'info, dlmm_pro::accounts::Pool>: it checks the owner and the discriminator and maps only the header bytes. That is enough for has_one checks on the mints and reserves and for the tick_current_id, pool_status and token_x_flag fields.

The Generated CPI Returns No Data

No DLMM Pro instruction declares a return value in the IDL, so dlmm_pro::cpi::swap and the other functions return Result<()>. Read the outcome of a call from token balances or from your own accounts. See Reading the Result of a Swap.

Use declare_program! On-Chain, Not dlmm-pro-commons

Generate the bindings in your program with declare_program!. Use dlmm-pro-commons for off-chain work: planning accounts, quoting and tests. Reasons:
  • The crate describes itself as an off-chain commons library. It carries a quote engine, async transfer hook resolution and account parsers that an on-chain program does not need.
  • The example program below builds for SBF into a binary of about 160 KB. Adding dlmm-pro-commons and calling DynamicPool::deserialize and derive_tick_array from a handler also builds and adds roughly 15 to 20 KB. Exact sizes depend on the Anchor version and the code. That build needs ruint held at 1.17.2 or older (cargo update -p ruint --precise 1.17.2), because ruint 1.18 and newer require Rust 1.90 and platform tools v1.54 ship Rust 1.89.
  • The crate declares its own dlmm_pro module with declare_program!, so you would carry a second copy of the generated types next to your own.
  • Every derive_* helper calls find_program_address, which costs compute on-chain. Pass the addresses in, check them with Anchor constraints, and derive them off-chain.

CPI Instruction Scope

The IDL lists 39 callable instructions. A program that integrates DLMM Pro usually calls these: The remaining instructions are called the same way, with the account lists in the IDL: expand_position, remove_position, lock_permanent_position, lock_vesting_position, initialize_tick_array, initialize_tick_bitmap_extension, increase_oracle_length, the pool creation instructions and the reward authority instructions. See Program Instructions for what each one does and who signs it. The operator and admin instructions are not callable by integrators.

Event CPI Accounts

Every instruction tabulated on this page has the Anchor event CPI accounts at the end of its fixed accounts. initialize_tick_bitmap_extension has none. Most of them emit an event. refresh_fees_and_rewards takes the accounts but emits no event, and claim_reward emits only when the amount is above 0. The accounts come after every other fixed account and before the remaining accounts. In the generated struct they are the fields event_authority and program. The program calls itself to emit the event, so the DLMM Pro program account must be among the accounts you pass to the CPI. In your own #[derive(Accounts)] struct you can name the program field anything and type it Program<'info, dlmm_pro::program::DlmmPro>, as the examples do. See Program Events for the event layouts.

Remaining Accounts

An instruction that moves tokens takes a RemainingAccountsInfo argument, and swap and the liquidity instructions also read tick arrays. The argument is named remaining_accounts_info on swap and remaining_account_info on the position and limit order instructions. The program reads the remaining accounts in two steps:
  1. It consumes the transfer hook accounts, slice by slice in the order of RemainingAccountsInfo.slices. Each slice takes length accounts from the front of the remaining accounts.
  2. It treats what is left as the tick arrays of the instruction.
RemainingAccountsInfo { slices: vec![] } means no hook accounts, and the tick arrays start at the first remaining account. A slice has an accounts_type and a length (u8). A slice of length 0 of an accepted type is skipped. A type the instruction does not accept fails with InvalidRemainingAccountSlice whatever its length. Two non-empty slices of the same type fail with DuplicatedRemainingAccountTypes. Slices that ask for more accounts than were passed fail with InsufficientRemainingAccounts.

Slice Types per Instruction

A mint with a transfer hook needs a slice for each leg that moves a non-zero amount of it, or the call fails with MissingRemainingAccountForTransferHook. A slice for a mint without a hook fails with NoTransferHookProgram. swap always makes its input and output transfers, so a hook mint on either side always needs its slice. The hook accounts of a leg are what the hook program declares for that transfer, so they depend on the transfer’s source, destination, authority and amount: An off-chain planner resolves these accounts with the transfer hook interface. get_extra_account_metas_for_transfer_hook in the commons library takes exactly this source, destination, authority and amount. getExtraAccountMetasForTransferHook in the SDK takes the mint, source, destination and authority and resolves the accounts for an amount of 0, so supply the metas yourself for a hook whose extra accounts depend on the amount. Your program does not resolve or inspect them. It forwards the remaining accounts and the RemainingAccountsInfo it received. A transfer that does not happen, such as a claim of zero, does not use its hook accounts.

Tick Arrays

payer pays for the tick arrays that open_position and open_limit_order create and for the tick entries they add to existing arrays. Pass the PDA address of each array even though the account does not exist yet. A tick array that does not belong to the pool, or an address that is not the next one expected, fails with InvalidTickArray. When the arrays you pass lack a tick that a position or order uses, the call fails with CannotFindTickFromId. A swap walks the remaining accounts in order and stops after the last array that holds liquidity in the swap direction. Accounts after that point are not read. Pass only tick arrays after the hook accounts. The extension account tick_array_bitmap_extension is needed whenever a tick array index lies outside -512 to 511. Pass it for any pool whose extension exists. Without it a swap fails with BitmapExtensionAccountIsNotProvided when its search leaves the inline range. A liquidity or limit order instruction fails with TickArrayBitmapExtensionAccountIsNotProvided when a tick array outside that range changes between empty and non-empty.

Limits

A swap uses 17 fixed accounts plus the hook accounts and tick arrays, and your own instruction adds its accounts on top. Every account of a transaction is paid for in the transaction size, so use a v0 transaction with an address lookup table when the account set grows. With your program as the top-level instruction, a swap through a Token 2022 mint with a transfer hook reaches depth 4 at the hook program: your program is depth 1, DLMM Pro depth 2, Token 2022 depth 3 and the hook depth 4. The event CPI that DLMM Pro makes to itself is depth 3. If another program calls yours, each added level moves every one of those calls one level deeper, and the hook program itself loses a level for its own CPIs. Compute cost depends on the position width, the number of tick arrays and the hook programs. Simulate the transaction on devnet with the accounts and the hooks you will use, and set the compute budget from the result. Wide positions, many tick arrays and hook programs raise the cost.

Account Reference

Tables list the accounts in IDL order. Mut means the account is writable in the CPI, and a writable account must also be writable in the outer transaction. Optional accounts are marked in the notes.

Swap Accounts

Arguments: parameters: SwapParameters { input_amount, minimum_output_amount } and remaining_accounts_info. The direction is not an argument. Remaining accounts: hook accounts for the slices above, then the tick arrays.

Position Accounts

open_position creates the position NFT as a Token 2022 mint with metadata and mints one token to the owner’s associated token account. Its argument is OpenPositionParameters { tick_ranges }, with 1 to 45 ranges. Each range needs a lower tick below the upper tick, and both must be usable tick ids on the pool’s tick spacing.
The IDL’s PDA hint for nft_account in open_position names the legacy Token program. The account the program expects is the associated token account under Token 2022, which is what findNftAccount in the SDK derives (with the owner allowed to be off-curve). Derive it with get_associated_token_address_with_program_id(owner, nft_mint, token_2022_program).
Remaining accounts: the tick arrays, ascending. The call fails with InvalidActivationAccess when bit 1 of pool_status is set, or when the activation rules do not let owner add liquidity yet. close_position closes an empty position: every slot must hold no liquidity, no pending fees and no pending rewards, or the call fails with PositionIsNotEmpty. It burns the NFT, closes the NFT token account, the mint and the position array, releases the position’s ticks and collects the freed tick array rent, and sends all of it to rent_receiver. Remaining accounts: the tick arrays of every tick of the position, ascending, one per distinct index.

Add and Remove Liquidity Accounts

add_liquidity, add_liquidity_by_weight and add_liquidity_imbalance share one account list. Each deposits into slots of an existing position: add_liquidity takes a bitmap and one liquidity amount per selected slot, add_liquidity_by_weight takes a multiplier and one weight per slot, and add_liquidity_imbalance deposits into one slot with optional per-token caps and a slippage check. Position slots are selected by bit i of a u64 bitmap, and expected_layout_version (Option<u16>) fails with InvalidLayoutVersion when it differs from the position’s layout_version. Pass None to skip the check. remove_liquidity and remove_liquidity_by_bps share another list. remove_liquidity takes a bitmap and one liquidity amount per selected slot. remove_liquidity_by_bps takes bps of the unlocked liquidity of each selected slot, or of every slot when bitmap is None. Both pay out through the pool authority. Remaining accounts for both: hook accounts for X and Y, then the tick arrays that hold both endpoints of every selected slot, in any order and all writable. Every remaining account after the hook slices is read as a tick array of the pool, so pass nothing else.

Fee and Reward Accounts

refresh_fees_and_rewards accrues fees and rewards into the selected slots and moves no tokens. Its argument is bitmap, which needs at least one bit set and no bit beyond the number of slots, or the call fails with InvalidParameters. Any NFT delegate may call it. Remaining accounts: the tick arrays of the selected slots, read-only is enough. claim_fee pays out the pending fees of every slot of the position. It does not refresh: fees that have accrued but are not yet credited stay unpaid until you call refresh_fees_and_rewards. Its arguments are min_amount_x and min_amount_y, which compare against the amount after the transfer fee and are skipped when 0, and remaining_account_info. claim_reward pays out one reward index, summed over every slot, on a pool with the LiquidityMining extension. Its arguments are reward_index (0 or 1), min_amount and remaining_account_info. With the derived reward vault, a pool without the extension or a slot that was never set up fails first with the Anchor error AccountNotInitialized (3012), because the vault does not exist. With any other token account in its place it fails with InvalidPoolType on a pool without the extension, RewardUninitialized for a slot that was never set up, and InvalidRewardMint or InvalidRewardVault when the accounts do not match the slot. No event is emitted and no transfer is made when the amount is 0. Remaining accounts: hook accounts in a TransferHookReward slice when the reward mint has a hook.

Limit Order Accounts

open_limit_order mints a limit order NFT and deposits one token into up to 25 ticks. An ask sells X above the current tick and a bid buys X below it. Its arguments are OpenLimitOrderParameters { is_ask_side, max_amount, relative_tick, ticks } and remaining_account_info. max_amount caps the deposit including the transfer fee. ticks must be strictly ascending with non-zero amounts. Ask ticks must lie above the current tick and bid ticks at or below it. The deposit is paid from user_token with owner as the transfer authority, so the owner must be able to sign for it. Remaining accounts: a hook slice for the deposited token, then the tick arrays. cancel_limit_order cancels the listed ticks and pays out the unfilled amount, the filled proceeds and the owner’s share of the fee. Its arguments are CancelLimitOrderParameters { tick_ids, min_amount_x, min_amount_y }, with tick_ids strictly ascending, and remaining_account_info. A tick that has nothing left to cancel fails with CancelNonEmptyLimitOrder, whose message reads “Cancel on empty limit order”. Remaining accounts: hook accounts for X, Y and the referral, then the tick arrays of the canceled ticks, in any order and writable. close_limit_order closes an order whose ticks are all canceled, or fails with LimitOrderIsNotEmpty. It burns the NFT, closes the NFT account, the mint and the order, collects the tick array rent and sends the rent to rent_receiver. Remaining accounts: the tick arrays of every tick the order holds, ascending, one per distinct index.

Signers and Delegates

The owner of a position or order is the owner of the NFT token account. When the owner is a PDA of your program, the PDA signs with CpiContext::new_with_signer, and the NFT account is a token account whose authority is that PDA. The delegate route lets a program act on a position that a user keeps in their own wallet:
  1. The owner approves the program’s PDA as the delegate of the NFT token account with a Token 2022 approve of amount 0. A non-zero delegated amount fails with DelegatedAmountNonZero, and a signer that is not the account’s delegate fails with InvalidAuthority.
  2. The owner calls update_position_delegate_permission (or update_limit_order_delegate_permission) with a permission bitmask. Position permissions are bit 0 AddLiquidity, bit 1 RemoveLiquidity, bit 2 ClaimFee, bit 3 ClaimReward and bit 4 UpdatePositionRange, so the largest mask is 31. A limit order has bit 0 CancelLimitOrder, so the largest mask is 1. A larger value fails with InvalidPermission. The permissions are stored on the position or order, not on the delegate.
  3. The PDA signs the instruction as signer. A delegate without the right bit fails with InvalidPermission.
When a delegate signs an instruction that pays tokens out, the receiving token account must be the owner’s associated token account for that mint and token program, and its authority must be the owner. Anything else fails with IncorrectATA. This applies to the payouts of remove_liquidity, remove_liquidity_by_bps, claim_fee, claim_reward and cancel_limit_order. A delegate that adds liquidity pays from token accounts it controls itself. The second example program below shows both the approve call and a claim signed by a PDA that owns the position.

A Complete Swap Program

This Anchor program swaps through DLMM Pro with two entry points: swap_exact_in for a wallet that signs the transaction, and swap_from_vault for a vault PDA of the program that owns the token accounts. Both go through one function that builds dlmm_pro::cpi::accounts::Swap, forwards the remaining accounts, passes the signer seeds to CpiContext::new_with_signer, and reads the result from the token balances. The program ID ULkDzAvpWkDRQAJRftnv5mAWcP1UHM85k9TYk6Ro1ze is a placeholder for your own.
What the example does and why:
  • pool is an AccountLoader with has_one constraints on the reserves and mints, so the accounts come from the pool and not from the caller. token_x_program and token_y_program are tied to the mints with mint::token_program, and the mint owner equals the program that the pool’s token_x_flag and token_y_flag name.
  • user is an unchecked account, and the two token accounts must have user as their authority. swap_exact_in requires that user signed. swap_from_vault requires that user is the vault PDA and signs for it with the seeds.
  • tick_array_bitmap_extension and referral are Option fields. The CPI struct takes Option<AccountInfo> for them, and None is sent as the DLMM Pro program ID, read-only and unsigned, which the program reads as absent.
  • The tick arrays and hook accounts are the remaining accounts. The function forwards ctx.remaining_accounts with with_remaining_accounts. A forwarded account is writable in the CPI only if it was writable in the outer transaction.
  • remaining_accounts_info comes from the caller. It describes the hook slices, so it travels with the remaining accounts it describes.
  • minimum_output_amount is also passed through from the caller in this example. A program that swaps on its own account should compute or require a bound itself and not forward zero.
  • After the CPI the two token accounts are reloaded, because Anchor caches the account data it loaded at the start of the instruction. The differences are the amount that left user_token_in and the amount that reached user_token_out.
This program was built for SBF with cargo build-sbf and run in LiteSVM against the DLMM Pro program binary from the SDK repository. A swap of 50,000 units of token X for token Y signed by a wallet, and a swap of 30,000 units of token Y for token X signed by the vault PDA, both moved exactly the input amount and delivered the amount the quote predicted.

Claiming Fees as the Owner and Approving a Delegate

This second program shows the other two patterns of the section above: a PDA that owns a position claims its fees into token accounts that the PDA owns, and the PDA approves a delegate on its NFT account with an amount of 0. Both use the vault PDA of the seed vault.
Call refresh_fees_and_rewards for the slots that hold liquidity before claim_fee, in the same transaction, because the claim pays only what is already credited. This program was built for SBF and run in LiteSVM against the DLMM Pro binary from the SDK repository: the claim signed by the vault PDA and the approve both succeed.

Off-Chain Account Planning

Plan the accounts off-chain and have your client pass them to your program. The TypeScript SDK builds a correct instruction. Print its account metas and mirror them in the accounts your program forwards. The SDK may add token account instructions around the swap, so the snippet picks the instruction that targets the DLMM Pro program. The first 17 accounts of a swap are the fixed accounts in IDL order, and the rest are the remaining accounts: hook accounts, then tick arrays.
The TypeScript client puts the program ID in the slot of an absent optional account and marks it read-only and not a signer. The generated Rust CPI struct does the same. The commons crate does the same planning in Rust. build_swap_instruction in the commons library page returns an Instruction whose first 17 account metas are the fixed accounts of swap and whose remaining metas are the hook accounts and tick arrays. Send those remaining metas to your program as its remaining accounts. For the other instructions use tick_arrays_for_tick_ids for positions and limit orders, get_extra_account_metas_for_transfer_hook for hook accounts and remaining_accounts_from_hooks for the RemainingAccountsInfo argument.

What a Calling Program Must Check or Pass Through

Reading the Result of a Swap

swap returns no data. Compare the balances of user_token_in and user_token_out before and after the CPI, as the example program does.
  • user_token_in drops by input_amount. The program transfers the whole amount from the user, and a Token 2022 transfer fee is withheld from what the pool receives.
  • user_token_out rises by the amount after the output mint’s transfer fee. That equals transfer_fee_excluded_out_amount of the quote and amount_out of the EvtSwap event, and it is the number that minimum_output_amount is compared with.
  • If a referral account is passed and a host fee is due, the fee goes to that account. With a separate referral account the swapper’s amounts are unchanged. When the referral is user_token_out or user_token_in itself, that balance also moves by the host fee, so the balance difference no longer equals amount_out or input_amount. Pass a separate referral account if your program accounts by balance difference.
The event is an inner instruction of your transaction that an off-chain reader can decode, but a program cannot read inner instructions of its own CPI.

Common CPI Failures

Program errors come from the IDL. Anchor errors come from account checks, in DLMM Pro’s accounts or in yours. A custom error that DLMM Pro returns passes through your program unchanged, so the code you see in the transaction is the code in the table. Two runtime errors are not DLMM Pro or Anchor errors. A CPI cannot make an account writable or a signer that the outer instruction did not mark that way, so forward only accounts that the transaction already marked. A PDA signer only counts when you pass its seeds to new_with_signer. A missing account in the CPI account infos fails before DLMM Pro runs.

Best Practices

Testing

LiteSVM

The SDK repository ships the program binaries its own tests run against. tests/fixtures/dlmm_pro.so is the DLMM Pro 0.1.0 binary and tests/fixtures/access_registry.so is the access registry binary, both built with the local feature according to the commons README. That build lets any wallet create an operator, which the deployed program does not. The TypeScript tests and the Rust tests in commons/tests/onchain load them into LiteSVM and create pools, positions and swaps against them. To test your program the same way:
  1. Build your program with cargo build-sbf.
  2. Load dlmm_pro.so at the program ID dPro1D5wiqnGMH8vMDG23jh35btTHoWdyqXjqkqxe8n, the access registry at regUFehW9oycnaV2auYyfqyYjKG1W1uLNT7rtUeemQr, and your own .so at your program ID. LiteSVM provides the Token, Token 2022, associated token and memo programs.
  3. Set the clock to a realistic time, because fees, activation and the oracle read it.
  4. Create a pool the way the SDK tests do, add liquidity, then build your instruction with the generated accounts and instruction types of your crate and send it.
  5. Compare the result with a quote from quote_exact_in.
The helper needs these dev-dependencies, the versions the SDK’s own tests use. The last line holds the runtime crates at 4.1.x, which litesvm 0.15.2 needs to compile.
The FIXTURES path assumes a checkout of the SDK repository next to your project. The helper functions that create pools and seed liquidity in commons/tests/onchain/common/mod.rs are a working reference for steps 4 and 5.

Devnet

The program has the same address on devnet. Create a test pool and seed it with liquidity using the SDK, then call your program with the accounts printed by the TypeScript snippet above. Use simulateTransaction first and read the logs for the error name and code. Compare the account list of your CPI with the SDK-built instruction when a call fails.

Rust Commons Library

Account parsers, quotes and the planning helpers used off-chain.

Program Instructions

Every instruction, its signers and the SDK builder that wraps it.

Program Errors

Error codes, messages and the usual causes.

Program Accounts

PDA seeds, account layouts and the dynamic tails.