| name | cdpm-user-sdk |
| description | TypeScript SDK guide for CDPM (Cetus DLMM Position Manager) end-users. Provides PTB construction patterns for creating positions, managing liquidity, authorizing agents, collecting fees, and supplying/redeeming idle funds via Scallop lending or Kai SAV lending. Use when users need to interact with CDPM contract through TypeScript SDK. |
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.
Package Address: 0x573584cc4698e82fd85f2b54e64ad4cd901c42b768f7628ec167bf2d24aa2aa7 (only-dep-upgrades digest: F5kVa3YDSHoBvJvYJFH9y5dANCJScEdyZoxZLLy6qd15 — cdpm.move bytecode is locked; only dependency-version upgrades are allowed). Other shared object IDs live in reference/constants.md.
The PositionManager struct contains a lending: Bag holding both Scallop ScallopVault<T> entries (keyed by type_name<T>) and Kai SAV KaiVault<T, YT> entries (keyed by type_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',
});
const CDPM_PACKAGE = '0x573584cc4698e82fd85f2b54e64ad4cd901c42b768f7628ec167bf2d24aa2aa7';
Topics
Core Operations
Agent & Fee Management
Scallop Lending (Idle Funds)
- Scallop Lending - Single MoveCall
scallop_supply<T> / scallop_redeem<T> against Scallop Market; yield-fee math on the interest portion; exit only via scallop_redeem<T>.
Kai SAV Lending (Idle Funds)
- Kai SAV Lending - Single MoveCall
kai_supply<T, YT> / kai_redeem<T, ST, YT> against Kai Vault<T, YT>; shared yield-fee math; exit only via kai_redeem<T, ST, YT>.
Web Development & Queries
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'
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
) {
const { response: pm } = await client.getObject({ id: pmId, include: { content: true } });
const owner = pm?.content?.fields?.owner;
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')) {
console.error('Only the owner can perform this operation');
} else if (e.message.includes('ENotAllow')) {
console.error('Caller not authorized (not owner / agent / whitelisted protocol with no agents set)');
} else if (e.message.includes('EInvalidFeeRate')) {
console.error('Invalid fee rate configuration (cap is 50% / 5000 bp)');
} else if (e.message.includes('ELendingNotEmpty')) {
console.error('pm.lending is non-empty — redeem every Scallop AND Kai vault entry before user_close_pm');
} else if (e.message.includes('ENoSuchVault')) {
console.error('No ScallopVault<T> or KaiVault<T, YT> entry in pm.lending for the requested key');
} else if (e.message.includes('ENoSuchBalance')) {
console.error('withdraw_from_balance / withdraw_from_fee called for an absent type key');
} else if (e.message.includes('EPositionHasRewards')) {
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')) {
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')) {
console.error('user_close_pm aborted: drain every pm.fee[T] with user_withdraw_fee<T>(u64::MAX)');
} 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.