dloom_flow/dlmm/instructions/
modify_liquidity.rs

1// FILE: programs/dloom_flow/src/dlmm/instructions/modify_liquidity.rs
2
3use crate::{
4    dlmm::{
5        math,
6        state::{Bin, DlmmPool, Position},
7    },
8    errors::DloomError,
9    events::DlmmLiquidityModified,
10    state::TransactionBins,
11};
12use anchor_lang::prelude::*;
13use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked};
14use std::collections::HashMap; // Added for validation
15
16pub fn handle_dlmm_modify_liquidity<'info>(
17    ctx: Context<'_, '_, 'info, 'info, DlmmModifyLiquidity<'info>>,
18    min_surplus_a_out: u64,
19    min_surplus_b_out: u64,
20) -> Result<()> {
21    // --- New Validation Step ---
22    // Create a HashMap for quick lookup of all provided bin accounts.
23    let account_infos_map: HashMap<Pubkey, &'info AccountInfo<'info>> = ctx
24        .remaining_accounts
25        .iter()
26        .map(|acc| (acc.key(), acc)) // Simply pass the reference, no .clone()
27        .collect();
28
29    // Verify that every pubkey cached in the transaction account was actually provided.
30    for bin_pubkey in &ctx.accounts.transaction_bins.bins {
31        require!(
32            account_infos_map.contains_key(bin_pubkey),
33            DloomError::BinCacheMismatch
34        );
35    }
36    // --- End Validation ---
37
38    let old_position_state = &*ctx.accounts.old_position;
39    let new_position_state = &*ctx.accounts.new_position;
40    let dlmm_pool_state = &*ctx.accounts.dlmm_pool;
41    let bin_step = dlmm_pool_state.bin_step as i32;
42    let liquidity_to_move = old_position_state.liquidity;
43    require!(liquidity_to_move > 0, DloomError::PositionNotEmpty);
44
45    // --- Calculations (Largely unchanged) ---
46    // 1. Calculate what is being withdrawn from the old position.
47    let (principal_a, principal_b) =
48        math::calculate_claimable_amounts(dlmm_pool_state, old_position_state, liquidity_to_move)?;
49
50    let mut total_fees_a: u128 = 0;
51    let mut total_fees_b: u128 = 0;
52
53    // 2. Separate the cached bin pubkeys for the old and new positions.
54    let expected_old_bins_count =
55        ((old_position_state.upper_bin_id - old_position_state.lower_bin_id) / bin_step + 1)
56            as usize;
57    let (old_bins_pubkeys, new_bins_pubkeys) = ctx
58        .accounts
59        .transaction_bins
60        .bins
61        .split_at(expected_old_bins_count);
62
63    // 3. Process old bins: calculate fees and remove liquidity.
64    let liquidity_per_old_bin = liquidity_to_move
65        .checked_div(expected_old_bins_count as u128)
66        .ok_or(DloomError::MathOverflow)?;
67
68    for bin_pubkey in old_bins_pubkeys.iter() {
69        let bin_info = account_infos_map.get(bin_pubkey).unwrap(); // Safe due to validation
70        let bin_loader = AccountLoader::<'_, Bin>::try_from(bin_info)?;
71        let mut bin = bin_loader.load_mut()?;
72        let (fees_a, fees_b) = math::calculate_accrued_fees(old_position_state, &bin);
73        total_fees_a = total_fees_a
74            .checked_add(fees_a as u128)
75            .ok_or(DloomError::MathOverflow)?;
76        total_fees_b = total_fees_b
77            .checked_add(fees_b as u128)
78            .ok_or(DloomError::MathOverflow)?;
79        bin.liquidity = bin
80            .liquidity
81            .checked_sub(liquidity_per_old_bin)
82            .ok_or(DloomError::MathOverflow)?;
83    }
84
85    let total_claimable_a = principal_a
86        .checked_add(total_fees_a)
87        .ok_or(DloomError::MathOverflow)?;
88    let total_claimable_b = principal_b
89        .checked_add(total_fees_b)
90        .ok_or(DloomError::MathOverflow)?;
91
92    // 4. Calculate token amounts required for the new position.
93    let (required_a, required_b) = math::calculate_required_token_amounts(
94        dlmm_pool_state,
95        new_position_state.lower_bin_id,
96        new_position_state.upper_bin_id,
97        liquidity_to_move,
98    )?;
99
100    // 5. Calculate surplus to be sent back to the user.
101    let surplus_a = total_claimable_a
102        .checked_sub(required_a)
103        .ok_or(DloomError::MathOverflow)?;
104    let surplus_b = total_claimable_b
105        .checked_sub(required_b)
106        .ok_or(DloomError::MathOverflow)?;
107    require!(
108        surplus_a >= min_surplus_a_out as u128 && surplus_b >= min_surplus_b_out as u128,
109        DloomError::SlippageExceeded
110    );
111
112    // --- CPIs & State Updates (Largely unchanged) ---
113    // 6. Prepare signer seeds.
114    let bin_step_bytes = &dlmm_pool_state.bin_step.to_le_bytes()[..];
115    let bump = &[dlmm_pool_state.bump][..];
116    let signer_seeds = &[
117        b"dlmm_pool",
118        dlmm_pool_state.token_a_mint.as_ref(),
119        dlmm_pool_state.token_b_mint.as_ref(),
120        bin_step_bytes,
121        bump,
122    ][..];
123
124    // 7. Transfer surplus tokens back to the user.
125    if surplus_a > 0 {
126        token_interface::transfer_checked(
127            CpiContext::new_with_signer(
128                ctx.accounts.token_a_program.to_account_info(),
129                TransferChecked {
130                    from: ctx.accounts.token_a_vault.to_account_info(),
131                    to: ctx.accounts.user_token_a_account.to_account_info(),
132                    authority: ctx.accounts.dlmm_pool.to_account_info(),
133                    mint: ctx.accounts.token_a_mint.to_account_info(),
134                },
135                &[signer_seeds],
136            ),
137            surplus_a as u64,
138            ctx.accounts.token_a_mint.decimals,
139        )?;
140    }
141    if surplus_b > 0 {
142        token_interface::transfer_checked(
143            CpiContext::new_with_signer(
144                ctx.accounts.token_b_program.to_account_info(),
145                TransferChecked {
146                    from: ctx.accounts.token_b_vault.to_account_info(),
147                    to: ctx.accounts.user_token_b_account.to_account_info(),
148                    authority: ctx.accounts.dlmm_pool.to_account_info(),
149                    mint: ctx.accounts.token_b_mint.to_account_info(),
150                },
151                &[signer_seeds],
152            ),
153            surplus_b as u64,
154            ctx.accounts.token_b_mint.decimals,
155        )?;
156    }
157
158    // 8. Update state.
159    let dlmm_pool = &mut ctx.accounts.dlmm_pool;
160    dlmm_pool.reserves_a = dlmm_pool
161        .reserves_a
162        .checked_sub(surplus_a as u64)
163        .ok_or(DloomError::MathOverflow)?;
164    dlmm_pool.reserves_b = dlmm_pool
165        .reserves_b
166        .checked_sub(surplus_b as u64)
167        .ok_or(DloomError::MathOverflow)?;
168
169    ctx.accounts.old_position.liquidity = 0;
170
171    let new_position = &mut ctx.accounts.new_position;
172    new_position.liquidity = new_position
173        .liquidity
174        .checked_add(liquidity_to_move)
175        .ok_or(DloomError::MathOverflow)?;
176
177    let liquidity_per_new_bin = liquidity_to_move
178        .checked_div(new_bins_pubkeys.len() as u128)
179        .ok_or(DloomError::MathOverflow)?;
180    let mut snapshot_a: u128 = 0;
181    let mut snapshot_b: u128 = 0;
182
183    for bin_pubkey in new_bins_pubkeys.iter() {
184        let bin_info = account_infos_map.get(bin_pubkey).unwrap(); // Safe due to validation
185        let bin_loader = AccountLoader::<'_, Bin>::try_from(bin_info)?;
186        let mut bin = bin_loader.load_mut()?;
187        bin.liquidity = bin
188            .liquidity
189            .checked_add(liquidity_per_new_bin)
190            .ok_or(DloomError::MathOverflow)?;
191        snapshot_a = snapshot_a.max(bin.fee_growth_per_unit_a);
192        snapshot_b = snapshot_b.max(bin.fee_growth_per_unit_b);
193    }
194    new_position.fee_growth_snapshot_a = snapshot_a;
195    new_position.fee_growth_snapshot_b = snapshot_b;
196
197    emit!(DlmmLiquidityModified {
198        owner: ctx.accounts.owner.key(),
199        pool_address: ctx.accounts.dlmm_pool.key(),
200        old_position_address: ctx.accounts.old_position.key(),
201        new_position_address: ctx.accounts.new_position.key(),
202        liquidity_to_move,
203        surplus_a_out: surplus_a as u64,
204        surplus_b_out: surplus_b as u64,
205    });
206
207    Ok(())
208}
209
210#[derive(Accounts)]
211pub struct DlmmModifyLiquidity<'info> {
212    #[account(mut)]
213    pub owner: Signer<'info>,
214
215    #[account(mut)]
216    pub dlmm_pool: Box<Account<'info, DlmmPool>>,
217
218    #[account(
219        mut,
220        has_one = owner @ DloomError::Unauthorized,
221        constraint = old_position.pool == dlmm_pool.key() @ DloomError::InvalidPool
222    )]
223    pub old_position: Box<Account<'info, Position>>,
224
225    #[account(
226        mut,
227        has_one = owner @ DloomError::Unauthorized,
228        constraint = new_position.pool == dlmm_pool.key() @ DloomError::InvalidPool
229    )]
230    pub new_position: Box<Account<'info, Position>>,
231
232    /// The temporary account holding the pubkeys of all bins for both old and new positions.
233    #[account(
234        mut,
235        has_one = owner,
236        close = owner,
237        seeds = [b"transaction_bins", owner.key().as_ref()],
238        bump
239    )]
240    pub transaction_bins: Account<'info, TransactionBins>,
241
242    #[account(address = dlmm_pool.token_a_mint)]
243    pub token_a_mint: InterfaceAccount<'info, Mint>,
244    #[account(address = dlmm_pool.token_b_mint)]
245    pub token_b_mint: InterfaceAccount<'info, Mint>,
246
247    #[account(mut, token::mint = dlmm_pool.token_a_mint, has_one = owner)]
248    pub user_token_a_account: InterfaceAccount<'info, TokenAccount>,
249    #[account(mut, token::mint = dlmm_pool.token_b_mint, has_one = owner)]
250    pub user_token_b_account: InterfaceAccount<'info, TokenAccount>,
251
252    #[account(mut, address = dlmm_pool.token_a_vault)]
253    pub token_a_vault: InterfaceAccount<'info, TokenAccount>,
254    #[account(mut, address = dlmm_pool.token_b_vault)]
255    pub token_b_vault: InterfaceAccount<'info, TokenAccount>,
256
257    pub token_a_program: Interface<'info, TokenInterface>,
258    pub token_b_program: Interface<'info, TokenInterface>,
259}