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’sdeclare_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.
-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.
[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, sodlmm_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 withdeclare_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-commonsand callingDynamicPool::deserializeandderive_tick_arrayfrom a handler also builds and adds roughly 15 to 20 KB. Exact sizes depend on the Anchor version and the code. That build needsruintheld at 1.17.2 or older (cargo update -p ruint --precise 1.17.2), becauseruint1.18 and newer require Rust 1.90 and platform tools v1.54 ship Rust 1.89. - The crate declares its own
dlmm_promodule withdeclare_program!, so you would carry a second copy of the generated types next to your own. - Every
derive_*helper callsfind_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 aRemainingAccountsInfo 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:
- It consumes the transfer hook accounts, slice by slice in the order of
RemainingAccountsInfo.slices. Each slice takeslengthaccounts from the front of the remaining accounts. - 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.
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 withCpiContext::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:
- The owner approves the program’s PDA as the delegate of the NFT token account with a Token 2022
approveof amount 0. A non-zero delegated amount fails withDelegatedAmountNonZero, and a signer that is not the account’s delegate fails withInvalidAuthority. - The owner calls
update_position_delegate_permission(orupdate_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 withInvalidPermission. The permissions are stored on the position or order, not on the delegate. - The PDA signs the instruction as
signer. A delegate without the right bit fails withInvalidPermission.
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.
poolis anAccountLoaderwithhas_oneconstraints on the reserves and mints, so the accounts come from the pool and not from the caller.token_x_programandtoken_y_programare tied to the mints withmint::token_program, and the mint owner equals the program that the pool’stoken_x_flagandtoken_y_flagname.useris an unchecked account, and the two token accounts must haveuseras their authority.swap_exact_inrequires thatusersigned.swap_from_vaultrequires thatuseris the vault PDA and signs for it with the seeds.tick_array_bitmap_extensionandreferralareOptionfields. The CPI struct takesOption<AccountInfo>for them, andNoneis 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_accountswithwith_remaining_accounts. A forwarded account is writable in the CPI only if it was writable in the outer transaction. remaining_accounts_infocomes from the caller. It describes the hook slices, so it travels with the remaining accounts it describes.minimum_output_amountis 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_inand the amount that reacheduser_token_out.
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 seedvault.
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.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_indrops byinput_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_outrises by the amount after the output mint’s transfer fee. That equalstransfer_fee_excluded_out_amountof the quote andamount_outof theEvtSwapevent, and it is the number thatminimum_output_amountis 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_outoruser_token_initself, that balance also moves by the host fee, so the balance difference no longer equalsamount_outorinput_amount. Pass a separate referral account if your program accounts by balance difference.
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:
- Build your program with
cargo build-sbf. - Load
dlmm_pro.soat the program IDdPro1D5wiqnGMH8vMDG23jh35btTHoWdyqXjqkqxe8n, the access registry atregUFehW9oycnaV2auYyfqyYjKG1W1uLNT7rtUeemQr, and your own.soat your program ID. LiteSVM provides the Token, Token 2022, associated token and memo programs. - Set the clock to a realistic time, because fees, activation and the oracle read it.
- Create a pool the way the SDK tests do, add liquidity, then build your instruction with the generated
accountsandinstructiontypes of your crate and send it. - Compare the result with a quote from
quote_exact_in.
litesvm 0.15.2 needs to compile.
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. UsesimulateTransaction 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.
Related
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.

