CDPM User SDK Guide
Overview
CDPM (Cetus DLMM Position Manager) is a proxy contract for managing Cetus DLMM positions with support for user self-management, agent delegation, protocol-managed operations, and two optional lending integrations for idle funds: Scallop (single-generic <T> market coin) and Kai SAV (two-generic <T, YT> strategy-aggregating vault). Both integrations share pm.lending: Bag and a single fee_house.fee_rate knob.
Latest Package Address (v2): 0x70bd7eee646217a77502985113eec7a1d9c02ce09a187e5e5f50078ad36a13dc. Use this address for moveCall targets. The original package/type address is 0x612dfd45a2e350995d492a59b595e64ec07a2253912f9eb22c2fd5947c6135d6 and remains required for object/event type filters. Upgrade transaction Af6UArK3NcLXNWX5fNeitYBiAKdozHCuUh3qpMdUMevz links Cetus DLMM v10. Full deployment details live in reference/constants.md.
The
PositionManagerstruct contains alending: Bagholding both ScallopScallopVault<T>entries (keyed bytype_name<T>) and Kai SAVKaiVault<T, YT>entries (keyed bytype_name<YT>) — both can coexist on a single PM. See Scallop Lending and Kai SAV Lending for end-user PTB recipes.
Quick Start
Installation
bun add @mysten/sui
Initialize Client
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Transaction } from '@mysten/sui/transactions';
const client = new SuiGrpcClient({
baseUrl: 'https://fullnode.mainnet.sui.io:443',
network: 'mainnet',
});
// Function calls target the latest upgraded package.
const CDPM_PACKAGE = '0x70bd7eee646217a77502985113eec7a1d9c02ce09a187e5e5f50078ad36a13dc';
// Struct and event types stay anchored to the original package.
const CDPM_ORIGINAL_PACKAGE = '0x612dfd45a2e350995d492a59b595e64ec07a2253912f9eb22c2fd5947c6135d6';
Topics
Core Operations
- Creating Positions - First-time and existing user workflows
- Position Management - Add/remove liquidity, pool ID helpers
- Balance Management - Deposit to and withdraw from balance
Agent & Fee Management
- Agent Management - Authorize/revoke agents
- Fee Collection - Collect fees and rewards
Scallop Lending (Idle Funds)
- Scallop Lending - Single MoveCall
scallop_supply<T>/scallop_redeem<T>against ScallopMarket; yield-fee math on the interest portion; exit only viascallop_redeem<T>.
Kai SAV Lending (Idle Funds)
- Kai SAV Lending - Single MoveCall
kai_supply<T, YT>/kai_redeem<T, ST, YT>against KaiVault<T, YT>; shared yield-fee math; exit only viakai_redeem<T, ST, YT>.
Web Development & Queries
- Web Query Guide - GraphQL queries for PositionManagers
- Pool Query Guide - Query Cetus DLMM pools by coin types
Reference
- Constants - Package IDs, object IDs, token addresses
Calculations
For liquidity calculations, bin price math, position management, and fee calculations, use the cdpm-calculation skill with the Cetus DLMM SDK:
import { BinUtils, FeeUtils } from '@cetusprotocol/dlmm-sdk/utils'
// Common calculations
const qPrice = BinUtils.getQPriceFromId(binId, binStep)
const liquidity = BinUtils.getLiquidity(amountA, amountB, qPrice)
const binId = BinUtils.getBinIdFromPrice(price, binStep, true, decimalA, decimalB)
See cdpm-calculation skill for complete reference with formulas, examples, and best practices.
Security Checklist
Before authorizing an agent:
async function securityChecklist(
client: SuiGrpcClient,
pmId: string,
agentAddress: string
) {
// 1. Verify you are the owner
const { response: pm } = await client.getObject({ id: pmId, include: { content: true } });
const owner = pm?.content?.fields?.owner;
// 2. Check agent is not already authorized
const agents = await getAuthorizedAgents(client, pmId);
const isAuthorized = agents.includes(agentAddress);
return { owner, isAuthorized };
}
Error Handling
Common errors and solutions:
try {
const result = await createPositionSmart(/* ... */);
} catch (e) {
if (e.message.includes('ENotOwner')) { // 1001
console.error('Only the owner can perform this operation');
} else if (e.message.includes('ENotAllow')) { // 1002
console.error('Caller not authorized (not owner / agent / whitelisted protocol with no agents set)');
} else if (e.message.includes('EInvalidFeeRate')) { // 1003
console.error('Invalid fee rate configuration (cap is 50% / 5000 bp)');
} else if (e.message.includes('ELendingNotEmpty')) {// 1004
console.error('pm.lending is non-empty — redeem every Scallop AND Kai vault entry before user_close_pm');
} else if (e.message.includes('ENoSuchVault')) { // 1005
console.error('No ScallopVault<T> or KaiVault<T, YT> entry in pm.lending for the requested key');
} else if (e.message.includes('ENoSuchBalance')) { // 1006
console.error('withdraw_from_balance / withdraw_from_fee called for an absent type key');
} else if (e.message.includes('EPositionHasRewards')) { // 1007
console.error('user_close_pm aborted: collect every reward type on the pool with user_collect_reward<A,B,R> first');
} else if (e.message.includes('EBalanceNotEmpty')) { // 1008
console.error('user_close_pm aborted: drain every pm.balance[T] with user_remove_liquidity_from_balance<T>(u64::MAX)');
} else if (e.message.includes('EFeeNotEmpty')) { // 1009
console.error('user_close_pm aborted: drain every pm.fee[T] with user_withdraw_fee<T>(u64::MAX)');
} else if (e.message.includes('EPositionAlreadyExists')) { // 1010
console.error('agent_create_position aborted: pm.position is already Some. Destroy the current position first.');
} else if (e.message.includes('ENoPosition')) { // 1011
console.error('Operation requires an active position but pm.position is None. Create one via agent_create_position (agent) or user_deposit_liquidity (owner).');
} else if (e.message.includes('EWrongPool')) { // 1012
console.error('agent_create_position aborted: pool does not match PositionManager.pool_id.');
} else {
console.error('Transaction failed:', e);
}
}
End-to-End Workflow
For the full close-PM flow (collect rewards → redeem every lending entry → drain pm.balance / pm.fee → batched transferObjects → user_close_pm), see reference/workflows.md § Close Position Safely.