CDPM Protocol SDK Guide
Overview
CDPM (Cetus DLMM Position Manager) protocol layer provides managed liquidity services with fee extraction. This guide covers protocol integration, admin operations, and architecture details.
Latest Package Address (v2): 0x70bd7eee646217a77502985113eec7a1d9c02ce09a187e5e5f50078ad36a13dc. Use it for moveCall targets. Use the original package/type address 0x612dfd45a2e350995d492a59b595e64ec07a2253912f9eb22c2fd5947c6135d6 for object and event type filters. Upgrade transaction Af6UArK3NcLXNWX5fNeitYBiAKdozHCuUh3qpMdUMevz links Cetus DLMM v10 and retains dependency-only policy 192. Full deployment details live in ../cdpm-user-sdk/reference/constants.md.
import { Transaction } from '@mysten/sui/transactions';
import { SuiGrpcClient } from '@mysten/sui/grpc';
Topics
Architecture & Permissions
- Architecture - System components and data structures
- Permission System - Permission matrix and access requirements
- Fee Mechanics - Fee calculation and distribution
Operations
- Admin Operations - Set fee rate (cap 50%), manage AccessList, collect fees
- Protocol Operations - Protocol-managed liquidity operations, Cetus reward collection, and Scallop / Kai SAV supply / redeem
- Scallop Lending -
scallop_supply<T>/scallop_redeem<T>— onetx.moveCalleach; agents-empty gating; shared yield-fee math; trust-boundary discussion. - Kai SAV Lending -
kai_supply<T, YT>/kai_redeem<T, ST, YT>— onetx.moveCalleach; agents-empty gating; shared yield-fee math.
Reference
- Events - Admin, protocol, Scallop, and Kai operation events
- Constants - See user-sdk constants
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'
// Protocol-specific calculations
const qPrice = BinUtils.getQPriceFromId(binId, binStep)
const liquidity = BinUtils.getLiquidity(amountA, amountB, qPrice)
// Protocol fee calculation
const protocolFees = FeeUtils.getProtocolFees(feeA, feeB, protocolFeeRate)
See cdpm-calculation skill for complete reference with formulas and best practices.
Integration Guide
Move.toml Configuration
[package]
name = "YourProtocol"
version = "1.0.0"
edition = "2024.beta"
[dependencies]
CetusDlmm = { git = "https://github.com/CetusProtocol/cetus-dlmm-interface.git", subdir = "packages/dlmm", rev = "mainnet-v0.10.0" }
IntegerMate = { git = "https://github.com/CetusProtocol/integer-mate.git", rev = "mainnet-v1.3.0" }
MoveSTL = { git = "https://github.com/CetusProtocol/move-stl.git", rev = "mainnet-v1.3.0" }
CDPM = { git = "https://github.com/your-repo/cdpm.git", rev = "main" }
[addresses]
your_protocol = "0x0"
Querying Protocol State
async function getProtocolState(
client: SuiGrpcClient,
feeHouseId: string,
accessListId: string
) {
const [feeHouseResult, accessListResult] = await Promise.all([
client.getObject({ id: feeHouseId, include: { content: true } }),
client.getObject({ id: accessListId, include: { content: true } }),
]);
const feeHouse = feeHouseResult.response;
const accessList = accessListResult.response;
return {
feeRate: feeHouse?.content?.fields?.fee_rate,
protocolFees: feeHouse?.content?.fields?.fee,
allowedAddresses: accessList?.content?.fields?.allow,
};
}
Security Considerations
AdminCap Security
// Best practices for AdminCap management
const adminSecurity = {
// 1. Use multi-sig for AdminCap
useMultisig: true,
// 2. Set reasonable fee rate limits
maxFeeRate: 5000, // 50% — contract-enforced ceiling
// 3. Regular access list audits
auditInterval: 7 * 24 * 60 * 60 * 1000, // 7 days
// 4. Monitor protocol fee accumulation
feeCollectionThreshold: 10000n, // Collect when fees exceed threshold
};
Protocol Operation Checks
async function validateProtocolOperation(
client: SuiGrpcClient,
accessListId: string,
pmId: string,
protocolAddress: string
): Promise<{ valid: boolean; reason?: string }> {
// Check if in AccessList
const { response: accessList } = await client.getObject({
id: accessListId,
include: { content: true }
});
const allowed = accessList?.content?.fields?.allow || [];
if (!allowed.includes(protocolAddress)) {
return { valid: false, reason: 'Not in AccessList' };
}
// Check if agents are empty
const { response: pm } = await client.getObject({
id: pmId,
include: { content: true }
});
const agents = pm?.content?.fields?.agents || [];
if (agents.length > 0) {
return { valid: false, reason: 'Position has active agents' };
}
return { valid: true };
}
Error Handling
// Source: sources/cdpm.move — codes are SHARED between Scallop and Kai integrations.
const ERROR_CODES = {
ENotOwner: 1001, // Caller is not pm.owner (owner-only operations such as user_close_pm, user_insert_agent)
ENotAllow: 1002, // Caller not in agents / access list, or protocol-tier gating with non-empty pm.agents
EInvalidFeeRate: 1003, // admin_set_fee given rate > MAX_FEE_RATE (5000 / 50%)
ELendingNotEmpty: 1004, // user_close_pm called with non-empty lending Bag (any Scallop or Kai entry)
ENoSuchVault: 1005, // pull_from_scallop_lending or pull_from_kai_lending for an absent vault entry
ENoSuchBalance: 1006, // withdraw_from_balance / withdraw_from_fee for an absent type key
EPositionHasRewards: 1007, // user_close_pm / agent_destroy_position called with unclaimed Cetus pool rewards on PositionInfo.rewards_owned
EBalanceNotEmpty: 1008, // user_close_pm called with non-empty balance Bag
EFeeNotEmpty: 1009, // user_close_pm called with non-empty fee Bag
EPositionAlreadyExists: 1010, // agent_create_position called when position is already Some
ENoPosition: 1011, // operation requires an active position but position is None
EWrongPool: 1012, // agent_create_position called with a pool that doesn't match pm.pool_id
};
function parseError(error: string): string {
if (error.includes('ENotOwner')) {
return 'Operation requires owner permission';
} else if (error.includes('ENotAllow')) {
return 'Caller not in AccessList, or PositionManager has active agents (protocol tier requires pm.agents empty)';
} else if (error.includes('EInvalidFeeRate')) {
return 'Fee rate must be between 0 and 5000 (50% cap enforced by admin_set_fee)';
} else if (error.includes('ELendingNotEmpty')) {
return 'PositionManager.lending is non-empty; drain every ScallopVault<T> AND KaiVault<T, YT> entry before user_close_pm';
} else if (error.includes('ENoSuchVault')) {
return 'scallop_redeem / kai_redeem invoked for a (T) or (T, YT) pair with no entry in pm.lending';
} else if (error.includes('ENoSuchBalance')) {
return 'Internal withdraw_from_balance / withdraw_from_fee called for a coin type not present in the bag';
} else if (error.includes('EPositionHasRewards')) {
return 'user_close_pm aborted because Cetus PositionInfo.rewards_owned has nonzero entries — call user_collect_reward for each reward type first';
} else if (error.includes('EBalanceNotEmpty')) {
return 'user_close_pm aborted because pm.balance still holds at least one coin type — drain via user_remove_liquidity_from_balance first';
} else if (error.includes('EFeeNotEmpty')) {
return 'user_close_pm aborted because pm.fee still holds at least one coin type — drain via user_withdraw_fee first';
} else if (error.includes('EPositionAlreadyExists')) {
return 'agent_create_position aborted because pm.position is already Some — destroy the current position first (agent_destroy_position)';
} else if (error.includes('ENoPosition')) {
return 'Operation aborted because pm.position is None — create a position first (agent_create_position / user_deposit_liquidity)';
} else if (error.includes('EWrongPool')) {
return 'agent_create_position aborted because the pool does not match pm.pool_id';
}
return 'Unknown error';
}