diff --git a/controller/core/src/instructions/protocol_fee/withdraw_protocol_fees/mod.rs b/controller/core/src/instructions/protocol_fee/withdraw_protocol_fees/mod.rs new file mode 100644 index 00000000..ae6adc7c --- /dev/null +++ b/controller/core/src/instructions/protocol_fee/withdraw_protocol_fees/mod.rs @@ -0,0 +1,2 @@ +pub mod v1; +pub mod v2; diff --git a/controller/core/src/instructions/protocol_fee/withdraw_protocol_fees.rs b/controller/core/src/instructions/protocol_fee/withdraw_protocol_fees/v1.rs similarity index 100% rename from controller/core/src/instructions/protocol_fee/withdraw_protocol_fees.rs rename to controller/core/src/instructions/protocol_fee/withdraw_protocol_fees/v1.rs diff --git a/controller/core/src/instructions/protocol_fee/withdraw_protocol_fees/v2.rs b/controller/core/src/instructions/protocol_fee/withdraw_protocol_fees/v2.rs new file mode 100644 index 00000000..9b822c2b --- /dev/null +++ b/controller/core/src/instructions/protocol_fee/withdraw_protocol_fees/v2.rs @@ -0,0 +1,55 @@ +use generic_array_struct::generic_array_struct; + +use crate::instructions::generic::DiscmOnlyIxData; + +// Accounts + +#[generic_array_struct(builder pub)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct WithdrawProtocolFeesV2IxAccs { + /// The pool's state singleton PDA + pub pool_state: T, + + /// The pool's protocol fee beneficiary + pub beneficiary: T, + + /// INF token account to withdraw unclaimed protocol fees to + pub withdraw_to: T, + + /// INF token mint + pub inf_mint: T, + + /// INF token program + pub token_program: T, +} + +impl WithdrawProtocolFeesV2IxAccs { + #[inline] + pub const fn memset(val: T) -> Self { + Self([val; WITHDRAW_PROTOCOL_FEES_V2_IX_ACCS_LEN]) + } +} + +pub type WithdrawProtocolFeesV2IxKeys<'a> = WithdrawProtocolFeesV2IxAccs<&'a [u8; 32]>; + +pub type WithdrawProtocolFeesV2IxKeysOwned = WithdrawProtocolFeesV2IxAccs<[u8; 32]>; + +pub type WithdrawProtocolFeesV2IxAccFlags = WithdrawProtocolFeesV2IxAccs; + +pub const WITHDRAW_PROTOCOL_FEES_V2_IX_IS_WRITER: WithdrawProtocolFeesV2IxAccFlags = + WithdrawProtocolFeesV2IxAccFlags::memset(false) + .const_with_pool_state(true) + .const_with_withdraw_to(true) + .const_with_inf_mint(true); + +pub const WITHDRAW_PROTOCOL_FEES_V2_IX_IS_SIGNER: WithdrawProtocolFeesV2IxAccFlags = + WithdrawProtocolFeesV2IxAccFlags::memset(false).const_with_beneficiary(true); + +// Data + +pub const WITHDRAW_PROTOCOL_FEES_V2_IX_DISCM: u8 = 25; + +pub type WithdrawProtocolFeesV2IxData = DiscmOnlyIxData; + +pub const WITHDRAW_PROTOCOL_FEES_V2_IX_DATA_LEN: usize = WithdrawProtocolFeesV2IxData::DATA_LEN; diff --git a/controller/program/src/instructions/protocol_fee/mod.rs b/controller/program/src/instructions/protocol_fee/mod.rs index 3329747d..050b28e3 100644 --- a/controller/program/src/instructions/protocol_fee/mod.rs +++ b/controller/program/src/instructions/protocol_fee/mod.rs @@ -1,3 +1,3 @@ pub mod set_protocol_fee; pub mod set_protocol_fee_beneficiary; -pub mod withdraw_protocol_fee; +pub mod withdraw_protocol_fees; diff --git a/controller/program/src/instructions/protocol_fee/withdraw_protocol_fees/mod.rs b/controller/program/src/instructions/protocol_fee/withdraw_protocol_fees/mod.rs new file mode 100644 index 00000000..ae6adc7c --- /dev/null +++ b/controller/program/src/instructions/protocol_fee/withdraw_protocol_fees/mod.rs @@ -0,0 +1,2 @@ +pub mod v1; +pub mod v2; diff --git a/controller/program/src/instructions/protocol_fee/withdraw_protocol_fee.rs b/controller/program/src/instructions/protocol_fee/withdraw_protocol_fees/v1.rs similarity index 98% rename from controller/program/src/instructions/protocol_fee/withdraw_protocol_fee.rs rename to controller/program/src/instructions/protocol_fee/withdraw_protocol_fees/v1.rs index 09aaa60d..a7ee3bda 100644 --- a/controller/program/src/instructions/protocol_fee/withdraw_protocol_fee.rs +++ b/controller/program/src/instructions/protocol_fee/withdraw_protocol_fees/v1.rs @@ -1,7 +1,7 @@ use inf1_ctl_jiminy::{ account_utils::pool_state_v2_checked, err::Inf1CtlErr, - instructions::protocol_fee::withdraw_protocol_fees::{ + instructions::protocol_fee::withdraw_protocol_fees::v1::{ NewWithdrawProtocolFeesIxAccsBuilder, WithdrawProtocolFeesIxAccs, WithdrawProtocolFeesIxData, WITHDRAW_PROTOCOL_FEES_IX_IS_SIGNER, }, diff --git a/controller/program/src/instructions/protocol_fee/withdraw_protocol_fees/v2.rs b/controller/program/src/instructions/protocol_fee/withdraw_protocol_fees/v2.rs new file mode 100644 index 00000000..5e92669e --- /dev/null +++ b/controller/program/src/instructions/protocol_fee/withdraw_protocol_fees/v2.rs @@ -0,0 +1,112 @@ +use inf1_ctl_jiminy::{ + account_utils::{pool_state_v2_checked, pool_state_v2_checked_mut}, + err::Inf1CtlErr, + instructions::protocol_fee::withdraw_protocol_fees::v2::{ + NewWithdrawProtocolFeesV2IxAccsBuilder, WithdrawProtocolFeesV2IxAccs, + WITHDRAW_PROTOCOL_FEES_V2_IX_IS_SIGNER, + }, + keys::POOL_STATE_ID, + pda_onchain::POOL_STATE_SIGNER, + program_err::Inf1CtlCustomProgErr, + svc::InfCalc, + typedefs::pool_sv::PoolSvLamports, +}; +use jiminy_cpi::{ + account::{Abr, AccountHandle}, + program_error::{ProgramError, NOT_ENOUGH_ACCOUNT_KEYS}, + Cpi, +}; +use jiminy_sysvar_clock::Clock; +use sanctum_spl_token_jiminy::{ + instructions::mint_to::mint_to_ix_account_handle_perms, + sanctum_spl_token_core::instructions::mint_to::{MintToIxData, NewMintToIxAccsBuilder}, +}; + +use crate::{ + token::checked_mint_of, + verify::{verify_not_rebalancing_and_not_disabled, verify_pks, verify_signers}, +}; + +type WithdrawProtocolFeesV2IxAccounts<'acc> = WithdrawProtocolFeesV2IxAccs>; + +#[inline] +pub fn withdraw_protocol_fees_v2_checked<'acc>( + abr: &Abr, + accs: &[AccountHandle<'acc>], +) -> Result, ProgramError> { + let accs = accs.first_chunk().ok_or(NOT_ENOUGH_ACCOUNT_KEYS)?; + let accs = WithdrawProtocolFeesV2IxAccs(*accs); + + let pool = pool_state_v2_checked(abr.get(*accs.pool_state()))?; + let mint_acc = abr.get(*accs.inf_mint()); + + let expected_pks = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(&POOL_STATE_ID) + .with_beneficiary(&pool.protocol_fee_beneficiary) + .with_inf_mint(&pool.lp_token_mint) + .with_token_program(mint_acc.owner()) + // Free: the beneficiary is free to specify whatever INF token account to withdraw to + // In the case of an invalid INF token acc, token prog mint_to CPI will fail + .with_withdraw_to(abr.get(*accs.withdraw_to()).key()) + .build(); + verify_pks(abr, &accs.0, &expected_pks.0)?; + + verify_signers(abr, &accs.0, &WITHDRAW_PROTOCOL_FEES_V2_IX_IS_SIGNER.0)?; + + verify_not_rebalancing_and_not_disabled(pool)?; + + Ok(accs) +} + +#[inline] +pub fn process_withdraw_protocol_fees_v2( + abr: &mut Abr, + cpi: &mut Cpi, + accs: WithdrawProtocolFeesV2IxAccounts, + clock: &Clock, +) -> Result<(), ProgramError> { + let pool = pool_state_v2_checked_mut(abr.get_mut(*accs.pool_state()))?; + pool.release_yield(clock.slot) + .map_err(Inf1CtlCustomProgErr)?; + + let protocol_fee_lamports = pool.protocol_fee_lamports; + + if protocol_fee_lamports == 0 { + return Ok(()); + } + + let pool_lamports = PoolSvLamports::from_pool_state_v2(pool); + let inf_token_supply = checked_mint_of(abr.get(*accs.inf_mint()))?.supply(); + + let inf_calc = InfCalc { + pool_lamports, + mint_supply: inf_token_supply, + }; + + let inf_to_mint = inf_calc + .sol_to_inf(protocol_fee_lamports) + .ok_or(Inf1CtlCustomProgErr(Inf1CtlErr::MathError))?; + + if inf_to_mint == 0 { + return Ok(()); + } + + cpi.invoke_signed_handle( + abr, + *accs.token_program(), + MintToIxData::new(inf_to_mint).as_buf(), + mint_to_ix_account_handle_perms( + NewMintToIxAccsBuilder::start() + .with_auth(*accs.pool_state()) + .with_mint(*accs.inf_mint()) + .with_to(*accs.withdraw_to()) + .build(), + ), + &[POOL_STATE_SIGNER], + )?; + + let pool = pool_state_v2_checked_mut(abr.get_mut(*accs.pool_state()))?; + pool.protocol_fee_lamports = 0; + + Ok(()) +} diff --git a/controller/program/src/lib.rs b/controller/program/src/lib.rs index 0e7de15e..5b8aab3f 100644 --- a/controller/program/src/lib.rs +++ b/controller/program/src/lib.rs @@ -22,7 +22,9 @@ use inf1_ctl_jiminy::instructions::{ protocol_fee::{ set_protocol_fee::SET_PROTOCOL_FEE_IX_DISCM, set_protocol_fee_beneficiary::SET_PROTOCOL_FEE_BENEFICIARY_IX_DISCM, - withdraw_protocol_fees::WITHDRAW_PROTOCOL_FEES_IX_DISCM, + withdraw_protocol_fees::{ + v1::WITHDRAW_PROTOCOL_FEES_IX_DISCM, v2::WITHDRAW_PROTOCOL_FEES_V2_IX_DISCM, + }, }, rebalance::{ end::END_REBALANCE_IX_DISCM, @@ -80,8 +82,9 @@ use crate::{ set_protocol_fee_beneficiary::{ process_set_protocol_fee_beneficiary, set_protocol_fee_beneficiary_accs_checked, }, - withdraw_protocol_fee::{ - process_withdraw_protocol_fees, withdraw_protocol_fees_checked, + withdraw_protocol_fees::{ + v1::{process_withdraw_protocol_fees, withdraw_protocol_fees_checked}, + v2::{process_withdraw_protocol_fees_v2, withdraw_protocol_fees_v2_checked}, }, }, rebalance::{ @@ -268,6 +271,13 @@ fn process_ix( let accs = set_rebal_auth_accs_checked(abr, accounts)?; process_set_rebal_auth(abr, accs) } + // v2 withdraw protocol fees + (&WITHDRAW_PROTOCOL_FEES_V2_IX_DISCM, _) => { + sol_log("WithdrawProtocolFeesV2"); + let accs = withdraw_protocol_fees_v2_checked(abr, accounts)?; + let clock = Clock::write_to(&mut clock)?; + process_withdraw_protocol_fees_v2(abr, cpi, accs, clock) + } // v2 swap (&SWAP_EXACT_IN_V2_IX_DISCM, data) => { sol_log("SwapExactInV2"); diff --git a/controller/program/tests/tests/protocol_fee/mod.rs b/controller/program/tests/tests/protocol_fee/mod.rs index 592be343..050b28e3 100644 --- a/controller/program/tests/tests/protocol_fee/mod.rs +++ b/controller/program/tests/tests/protocol_fee/mod.rs @@ -1,3 +1,3 @@ -mod set_protocol_fee; -mod set_protocol_fee_beneficiary; -mod withdraw_protocol_fees; +pub mod set_protocol_fee; +pub mod set_protocol_fee_beneficiary; +pub mod withdraw_protocol_fees; diff --git a/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees/mod.rs b/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees/mod.rs new file mode 100644 index 00000000..ae6adc7c --- /dev/null +++ b/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees/mod.rs @@ -0,0 +1,2 @@ +pub mod v1; +pub mod v2; diff --git a/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees.rs b/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees/v1.rs similarity index 99% rename from controller/program/tests/tests/protocol_fee/withdraw_protocol_fees.rs rename to controller/program/tests/tests/protocol_fee/withdraw_protocol_fees/v1.rs index 623c500b..050ba1f4 100644 --- a/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees.rs +++ b/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees/v1.rs @@ -2,7 +2,7 @@ use generic_array_struct::generic_array_struct; use inf1_ctl_jiminy::{ accounts::pool_state::{PoolStateV2, PoolStateV2Addrs, PoolStateV2FtaVals}, err::Inf1CtlErr, - instructions::protocol_fee::withdraw_protocol_fees::{ + instructions::protocol_fee::withdraw_protocol_fees::v1::{ NewWithdrawProtocolFeesIxAccsBuilder, WithdrawProtocolFeesIxAccsBuilder, WithdrawProtocolFeesIxData, WithdrawProtocolFeesIxKeysOwned, WITHDRAW_PROTOCOL_FEES_IX_ACCS_IDX_BENEFICIARY, diff --git a/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees/v2.rs b/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees/v2.rs new file mode 100644 index 00000000..d5212ee4 --- /dev/null +++ b/controller/program/tests/tests/protocol_fee/withdraw_protocol_fees/v2.rs @@ -0,0 +1,669 @@ +use inf1_ctl_jiminy::{ + accounts::pool_state::{ + PoolStateV2, PoolStateV2Addrs, PoolStateV2FtaVals, PoolStateV2Packed, PoolStateV2U64s, + }, + err::Inf1CtlErr, + instructions::protocol_fee::withdraw_protocol_fees::v2::{ + NewWithdrawProtocolFeesV2IxAccsBuilder, WithdrawProtocolFeesV2IxData, + WithdrawProtocolFeesV2IxKeysOwned, WITHDRAW_PROTOCOL_FEES_V2_IX_ACCS_IDX_BENEFICIARY, + WITHDRAW_PROTOCOL_FEES_V2_IX_ACCS_IDX_INF_MINT, + WITHDRAW_PROTOCOL_FEES_V2_IX_ACCS_IDX_POOL_STATE, + WITHDRAW_PROTOCOL_FEES_V2_IX_ACCS_IDX_WITHDRAW_TO, WITHDRAW_PROTOCOL_FEES_V2_IX_IS_SIGNER, + WITHDRAW_PROTOCOL_FEES_V2_IX_IS_WRITER, + }, + keys::POOL_STATE_ID, + program_err::Inf1CtlCustomProgErr, + svc::InfCalc, + typedefs::pool_sv::PoolSvLamports, +}; +use inf1_svc_ag_core::{calc::SvcCalcAg, inf1_svc_lido_core::solido_legacy_core::TOKENKEG_PROGRAM}; +use inf1_test_utils::{ + acc_bef_aft, any_normal_pk, any_pool_state_v2, assert_diffs_pool_state_v2, + assert_jiminy_prog_err, assert_token_acc_diffs, keys_signer_writable_to_metas, + mock_mint_with_prog, mock_sys_acc, mock_token_acc_with_prog, mollusk_exec, + pool_state_v2_account, pool_state_v2_u64s_solvent_strat, pool_state_v2_u8_bools_normal_strat, + raw_mint, raw_token_acc, silence_mollusk_logs, token_acc_bal_diff_changed, AccountMap, Diff, + DiffsPoolStateV2, PoolStateV2FtaStrat, ALL_FIXTURES, INF_MINT, +}; +use mollusk_svm::Mollusk; +use proptest::prelude::*; +use sanctum_spl_token_jiminy::sanctum_spl_token_core::state::{ + account::RawTokenAccount, + mint::{Mint, RawMint}, +}; +use sanctum_u64_ratio::Ratio; +use solana_instruction::Instruction; +use solana_pubkey::Pubkey; + +use crate::common::{header_lookahead, Cbs, SVM}; +use jiminy_cpi::program_error::{ProgramError, INVALID_ARGUMENT, MISSING_REQUIRED_SIGNATURE}; + +const INF_MINT_ID: [u8; 32] = INF_MINT.to_bytes(); + +/// Safety margin to prevent u64 overflow in sol_to_inf calculation +/// when protocol_fee_lamports * inf_mint_supply +const SAFE_MUL_U64_MAX: u64 = u32::MAX as u64; + +/// Lookahead to after release_yield with no LST updates +fn pool_state_header_lookahead(ps: PoolStateV2, curr_slot: u64) -> PoolStateV2 { + header_lookahead(ps, &[] as &[Cbs], curr_slot) +} + +fn withdraw_protocol_fees_v2_ix(keys: &WithdrawProtocolFeesV2IxKeysOwned) -> Instruction { + let accounts = keys_signer_writable_to_metas( + keys.0.iter(), + WITHDRAW_PROTOCOL_FEES_V2_IX_IS_SIGNER.0.iter(), + WITHDRAW_PROTOCOL_FEES_V2_IX_IS_WRITER.0.iter(), + ); + Instruction { + program_id: Pubkey::new_from_array(inf1_ctl_jiminy::ID), + accounts, + data: WithdrawProtocolFeesV2IxData::as_buf().into(), + } +} + +fn gen_inf_mint(supply: u64) -> RawMint { + raw_mint(Some(POOL_STATE_ID), None, supply, 9) +} + +fn withdraw_protocol_fees_v2_test_accs( + keys: &WithdrawProtocolFeesV2IxKeysOwned, + pool: PoolStateV2, + inf_mint_supply: u64, + withdraw_to_balance: u64, +) -> AccountMap { + const LAMPORTS: u64 = 1_000_000_000; + + let token_prog = *keys.token_program(); + let inf_mint_pk = *keys.inf_mint(); + + let accs = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(pool_state_v2_account(pool)) + .with_beneficiary(mock_sys_acc(LAMPORTS)) + .with_withdraw_to(mock_token_acc_with_prog( + raw_token_acc(inf_mint_pk, *keys.beneficiary(), withdraw_to_balance), + token_prog, + )) + .with_inf_mint(mock_mint_with_prog( + gen_inf_mint(inf_mint_supply), + TOKENKEG_PROGRAM, + )) + .with_token_program(ALL_FIXTURES.get(&TOKENKEG_PROGRAM.into()).unwrap().clone()) + .build(); + + keys.0.into_iter().map(Into::into).zip(accs.0).collect() +} + +fn withdraw_protocol_fees_v2_test( + svm: &Mollusk, + ix: Instruction, + bef: &AccountMap, + expected_err: Option>, +) { + let [pool_pk, withdraw_to_pk, inf_mint_pk] = [ + WITHDRAW_PROTOCOL_FEES_V2_IX_ACCS_IDX_POOL_STATE, + WITHDRAW_PROTOCOL_FEES_V2_IX_ACCS_IDX_WITHDRAW_TO, + WITHDRAW_PROTOCOL_FEES_V2_IX_ACCS_IDX_INF_MINT, + ] + .map(|i| ix.accounts[i].pubkey); + let result = mollusk_exec(svm, &[ix], bef); + + match expected_err { + None => { + let aft: AccountMap = result.unwrap().resulting_accounts; + + let [pool_state_bef, pool_state_aft] = { + acc_bef_aft(&pool_pk, bef, &aft).map(|acc| { + PoolStateV2Packed::of_acc_data(&acc.data) + .unwrap() + .into_pool_state_v2() + }) + }; + + let pool_state_bef = + pool_state_header_lookahead(pool_state_bef, svm.sysvars.clock.slot); + + let [withdraw_to_bef, withdraw_to_aft] = { + acc_bef_aft(&withdraw_to_pk, bef, &aft) + .map(|acc| RawTokenAccount::of_acc_data(&acc.data).unwrap()) + }; + + let [inf_mint_bef, inf_mint_aft] = { + acc_bef_aft(&inf_mint_pk, bef, &aft).map(|acc| { + RawMint::of_acc_data(&acc.data) + .and_then(Mint::try_from_raw) + .unwrap() + }) + }; + + let inf_calc = InfCalc { + pool_lamports: PoolSvLamports::from_pool_state_v2(&pool_state_bef), + mint_supply: inf_mint_bef.supply(), + }; + let expected_minted = inf_calc + .sol_to_inf(pool_state_bef.protocol_fee_lamports) + .unwrap(); + + assert_token_acc_diffs( + withdraw_to_bef, + withdraw_to_aft, + &token_acc_bal_diff_changed(withdraw_to_bef, expected_minted as i128), + ); + + assert_eq!( + inf_mint_aft.supply() - inf_mint_bef.supply(), + expected_minted + ); + + assert_diffs_pool_state_v2( + &DiffsPoolStateV2 { + u64s: PoolStateV2U64s::default().with_protocol_fee_lamports(Diff::Changed( + pool_state_bef.protocol_fee_lamports, + 0, + )), + ..Default::default() + }, + &pool_state_bef, + &pool_state_aft, + ); + + // assert redemption rate does not change + let [lp_due_bef, lp_due_aft] = [&pool_state_bef, &pool_state_aft].map(|ps| { + PoolSvLamports::from_pool_state_v2(ps) + .lp_due_checked() + .unwrap() + }); + + let lp_rate_bef = Ratio { + n: lp_due_bef, + d: inf_mint_bef.supply(), + }; + let lp_rate_aft_lenient = Ratio { + n: lp_due_aft.saturating_add(2), + d: inf_mint_aft.supply(), + }; + + assert!( + lp_rate_aft_lenient >= lp_rate_bef, + "LP redemption rate decreased: {:?} -> {:?}", + lp_rate_bef, + lp_rate_aft_lenient + ); + } + Some(e) => { + assert_jiminy_prog_err(&result.unwrap_err(), e); + } + } +} + +#[test] +fn withdraw_protocol_fees_v2_correct_basic() { + const INF_MINT_SUPPLY: u64 = 10_000_000_000_000; + const WITHDRAW_TO_BALANCE: u64 = 50; + const PROTOCOL_FEE_LAMPORTS: u64 = 1_000_000_000; + const TOTAL_SOL_VALUE: u64 = 100_000_000_000_000; + + // 69 + to avoid colliding with system prog + let [ben, wt] = core::array::from_fn(|i| [69 + u8::try_from(i).unwrap(); 32]); + + let pool = PoolStateV2FtaVals { + addrs: PoolStateV2Addrs::default() + .with_protocol_fee_beneficiary(ben) + .with_lp_token_mint(INF_MINT_ID), + u64s: PoolStateV2U64s::default() + .with_protocol_fee_lamports(PROTOCOL_FEE_LAMPORTS) + .with_total_sol_value(TOTAL_SOL_VALUE), + ..Default::default() + } + .into_pool_state_v2(); + + let keys = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(POOL_STATE_ID) + .with_beneficiary(ben) + .with_withdraw_to(wt) + .with_inf_mint(INF_MINT_ID) + .with_token_program(TOKENKEG_PROGRAM) + .build(); + + SVM.with(|svm| { + withdraw_protocol_fees_v2_test( + svm, + withdraw_protocol_fees_v2_ix(&keys), + &withdraw_protocol_fees_v2_test_accs(&keys, pool, INF_MINT_SUPPLY, WITHDRAW_TO_BALANCE), + Option::::None, + ); + }); +} + +fn to_inp( + (keys, pool, inf_mint_supply, withdraw_to_balance): ( + WithdrawProtocolFeesV2IxKeysOwned, + PoolStateV2, + u64, + u64, + ), +) -> (Instruction, AccountMap) { + ( + withdraw_protocol_fees_v2_ix(&keys), + withdraw_protocol_fees_v2_test_accs(&keys, pool, inf_mint_supply, withdraw_to_balance), + ) +} + +fn correct_strat() -> impl Strategy { + (0..=SAFE_MUL_U64_MAX) + .prop_flat_map(|tsv| { + pool_state_v2_u64s_solvent_strat(tsv) + .prop_flat_map(|solvent_u64s| { + ( + any_normal_pk(), + any_pool_state_v2(PoolStateV2FtaStrat { + u8_bools: pool_state_v2_u8_bools_normal_strat(), + addrs: PoolStateV2Addrs::default() + .with_lp_token_mint(Some(Just(INF_MINT_ID).boxed())), + u64s: PoolStateV2U64s::default() + .with_total_sol_value(Some( + Just(*solvent_u64s.total_sol_value()).boxed(), + )) + .with_withheld_lamports(Some( + Just(*solvent_u64s.withheld_lamports()).boxed(), + )) + .with_protocol_fee_lamports(Some( + Just(*solvent_u64s.protocol_fee_lamports()).boxed(), + )) + .with_last_release_slot(Some(Just(0).boxed())), + ..Default::default() + }), + 0..=SAFE_MUL_U64_MAX, + 0..=u64::MAX, + ) + }) + .prop_map(|(wt_pk, ps, inf_mint_supply, withdraw_to_balance)| { + let keys = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(POOL_STATE_ID) + .with_beneficiary(ps.protocol_fee_beneficiary) + .with_withdraw_to(wt_pk) + .with_inf_mint(INF_MINT_ID) + .with_token_program(TOKENKEG_PROGRAM) + .build(); + + (keys, ps, inf_mint_supply, withdraw_to_balance) + }) + }) + .prop_map(to_inp) +} + +proptest! { + #[test] + fn withdraw_protocol_fees_v2_correct_pt( + (ix, bef) in correct_strat(), + ) { + silence_mollusk_logs(); + SVM.with(|svm| { + withdraw_protocol_fees_v2_test(svm, ix, &bef, Option::::None); + }); + } +} + +fn zero_fees_strat() -> impl Strategy { + (0..=SAFE_MUL_U64_MAX) + .prop_flat_map(|tsv| { + pool_state_v2_u64s_solvent_strat(tsv) + .prop_flat_map(|solvent_u64s| { + ( + any_normal_pk(), + any_pool_state_v2(PoolStateV2FtaStrat { + u8_bools: pool_state_v2_u8_bools_normal_strat(), + addrs: PoolStateV2Addrs::default() + .with_lp_token_mint(Some(Just(INF_MINT_ID).boxed())), + u64s: PoolStateV2U64s::default() + .with_total_sol_value(Some( + Just(*solvent_u64s.total_sol_value()).boxed(), + )) + .with_withheld_lamports(Some( + Just(*solvent_u64s.withheld_lamports()).boxed(), + )) + .with_protocol_fee_lamports(Some(Just(0).boxed())) + .with_last_release_slot(Some(Just(0).boxed())), + ..Default::default() + }), + 0..=SAFE_MUL_U64_MAX, + 0..=u64::MAX, + ) + }) + .prop_map(|(wt_pk, ps, inf_mint_supply, withdraw_to_balance)| { + let keys = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(POOL_STATE_ID) + .with_beneficiary(ps.protocol_fee_beneficiary) + .with_withdraw_to(wt_pk) + .with_inf_mint(INF_MINT_ID) + .with_token_program(TOKENKEG_PROGRAM) + .build(); + + (keys, ps, inf_mint_supply, withdraw_to_balance) + }) + }) + .prop_map(to_inp) +} + +proptest! { + #[test] + fn withdraw_protocol_fees_v2_zero_fees_pt( + (ix, bef) in zero_fees_strat(), + ) { + silence_mollusk_logs(); + SVM.with(|svm| { + withdraw_protocol_fees_v2_test(svm, ix, &bef, Option::::None); + }); + } +} + +fn unauthorized_strat() -> impl Strategy { + (0..=SAFE_MUL_U64_MAX) + .prop_flat_map(|tsv| { + pool_state_v2_u64s_solvent_strat(tsv) + .prop_flat_map(|solvent_u64s| { + ( + any_pool_state_v2(PoolStateV2FtaStrat { + u8_bools: pool_state_v2_u8_bools_normal_strat(), + addrs: PoolStateV2Addrs::default() + .with_lp_token_mint(Some(Just(INF_MINT_ID).boxed())), + u64s: PoolStateV2U64s::default() + .with_total_sol_value(Some( + Just(*solvent_u64s.total_sol_value()).boxed(), + )) + .with_withheld_lamports(Some( + Just(*solvent_u64s.withheld_lamports()).boxed(), + )) + .with_protocol_fee_lamports(Some( + Just(*solvent_u64s.protocol_fee_lamports()).boxed(), + )) + .with_last_release_slot(Some(Just(0).boxed())), + ..Default::default() + }), + 0..=SAFE_MUL_U64_MAX, + 0..=u64::MAX, + ) + }) + .prop_flat_map(|(ps, inf_mint_supply, withdraw_to_balance)| { + ( + any_normal_pk().prop_filter("wrong beneficiary", move |pk| { + *pk != ps.protocol_fee_beneficiary + }), + any_normal_pk(), + Just(ps), + Just(inf_mint_supply), + Just(withdraw_to_balance), + ) + }) + .prop_map( + |(wrong_ben, wt_pk, ps, inf_mint_supply, withdraw_to_balance)| { + let keys = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(POOL_STATE_ID) + .with_beneficiary(wrong_ben) + .with_withdraw_to(wt_pk) + .with_inf_mint(INF_MINT_ID) + .with_token_program(TOKENKEG_PROGRAM) + .build(); + + (keys, ps, inf_mint_supply, withdraw_to_balance) + }, + ) + }) + .prop_map(to_inp) +} + +proptest! { + #[test] + fn withdraw_protocol_fees_v2_unauthorized_pt( + (ix, bef) in unauthorized_strat(), + ) { + silence_mollusk_logs(); + SVM.with(|svm| { + withdraw_protocol_fees_v2_test(svm, ix, &bef, Some(INVALID_ARGUMENT)); + }); + } +} + +fn missing_sig_strat() -> impl Strategy { + correct_strat().prop_map(|(mut ix, bef)| { + ix.accounts[WITHDRAW_PROTOCOL_FEES_V2_IX_ACCS_IDX_BENEFICIARY].is_signer = false; + (ix, bef) + }) +} + +proptest! { + #[test] + fn withdraw_protocol_fees_v2_missing_sig_pt( + (ix, bef) in missing_sig_strat(), + ) { + silence_mollusk_logs(); + SVM.with(|svm| { + withdraw_protocol_fees_v2_test(svm, ix, &bef, Some(MISSING_REQUIRED_SIGNATURE)); + }); + } +} + +fn disabled_strat() -> impl Strategy { + (0..=SAFE_MUL_U64_MAX) + .prop_flat_map(|tsv| { + pool_state_v2_u64s_solvent_strat(tsv) + .prop_flat_map(|solvent_u64s| { + ( + any_normal_pk(), + any_pool_state_v2(PoolStateV2FtaStrat { + u8_bools: pool_state_v2_u8_bools_normal_strat() + .with_is_disabled(Some(Just(true).boxed())), + addrs: PoolStateV2Addrs::default() + .with_lp_token_mint(Some(Just(INF_MINT_ID).boxed())), + u64s: PoolStateV2U64s::default() + .with_total_sol_value(Some( + Just(*solvent_u64s.total_sol_value()).boxed(), + )) + .with_withheld_lamports(Some( + Just(*solvent_u64s.withheld_lamports()).boxed(), + )) + .with_protocol_fee_lamports(Some(Just(0).boxed())) + .with_last_release_slot(Some(Just(0).boxed())), + ..Default::default() + }), + 0..=SAFE_MUL_U64_MAX, + 0..=u64::MAX, + ) + }) + .prop_map(|(wt_pk, ps, inf_mint_supply, withdraw_to_balance)| { + let keys = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(POOL_STATE_ID) + .with_beneficiary(ps.protocol_fee_beneficiary) + .with_withdraw_to(wt_pk) + .with_inf_mint(INF_MINT_ID) + .with_token_program(TOKENKEG_PROGRAM) + .build(); + + (keys, ps, inf_mint_supply, withdraw_to_balance) + }) + }) + .prop_map(to_inp) +} + +proptest! { + #[test] + fn withdraw_protocol_fees_v2_disabled_pt( + (ix, bef) in disabled_strat(), + ) { + silence_mollusk_logs(); + SVM.with(|svm| { + withdraw_protocol_fees_v2_test(svm, ix, &bef, Some(Inf1CtlCustomProgErr(Inf1CtlErr::PoolDisabled))); + }); + } +} + +fn rebalancing_strat() -> impl Strategy { + (0..=SAFE_MUL_U64_MAX) + .prop_flat_map(|tsv| { + pool_state_v2_u64s_solvent_strat(tsv) + .prop_flat_map(|solvent_u64s| { + ( + any_normal_pk(), + any_pool_state_v2(PoolStateV2FtaStrat { + u8_bools: pool_state_v2_u8_bools_normal_strat() + .with_is_rebalancing(Some(Just(true).boxed())), + addrs: PoolStateV2Addrs::default() + .with_lp_token_mint(Some(Just(INF_MINT_ID).boxed())), + u64s: PoolStateV2U64s::default() + .with_total_sol_value(Some( + Just(*solvent_u64s.total_sol_value()).boxed(), + )) + .with_withheld_lamports(Some( + Just(*solvent_u64s.withheld_lamports()).boxed(), + )) + .with_protocol_fee_lamports(Some(Just(0).boxed())) + .with_last_release_slot(Some(Just(0).boxed())), + ..Default::default() + }), + 0..=SAFE_MUL_U64_MAX, + 0..=u64::MAX, + ) + }) + .prop_map(|(wt_pk, ps, inf_mint_supply, withdraw_to_balance)| { + let keys = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(POOL_STATE_ID) + .with_beneficiary(ps.protocol_fee_beneficiary) + .with_withdraw_to(wt_pk) + .with_inf_mint(INF_MINT_ID) + .with_token_program(TOKENKEG_PROGRAM) + .build(); + + (keys, ps, inf_mint_supply, withdraw_to_balance) + }) + }) + .prop_map(to_inp) +} + +proptest! { + #[test] + fn withdraw_protocol_fees_v2_rebalancing_pt( + (ix, bef) in rebalancing_strat(), + ) { + silence_mollusk_logs(); + SVM.with(|svm| { + withdraw_protocol_fees_v2_test(svm, ix, &bef, Some(Inf1CtlCustomProgErr(Inf1CtlErr::PoolRebalancing))); + }); + } +} + +fn wrong_token_prog_strat() -> impl Strategy { + (0..=SAFE_MUL_U64_MAX) + .prop_flat_map(|tsv| { + pool_state_v2_u64s_solvent_strat(tsv) + .prop_flat_map(|solvent_u64s| { + ( + any_normal_pk(), + any_normal_pk() + .prop_filter("must be wrong token prog", |pk| *pk != TOKENKEG_PROGRAM), + any_pool_state_v2(PoolStateV2FtaStrat { + u8_bools: pool_state_v2_u8_bools_normal_strat(), + addrs: PoolStateV2Addrs::default() + .with_lp_token_mint(Some(Just(INF_MINT_ID).boxed())), + u64s: PoolStateV2U64s::default() + .with_total_sol_value(Some( + Just(*solvent_u64s.total_sol_value()).boxed(), + )) + .with_withheld_lamports(Some( + Just(*solvent_u64s.withheld_lamports()).boxed(), + )) + .with_protocol_fee_lamports(Some( + Just(*solvent_u64s.protocol_fee_lamports()).boxed(), + )) + .with_last_release_slot(Some(Just(0).boxed())), + ..Default::default() + }), + 0..=SAFE_MUL_U64_MAX, + 0..=u64::MAX, + ) + }) + .prop_map( + |(wt_pk, bad_token_prog, ps, inf_mint_supply, withdraw_to_balance)| { + let keys = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(POOL_STATE_ID) + .with_beneficiary(ps.protocol_fee_beneficiary) + .with_withdraw_to(wt_pk) + .with_inf_mint(INF_MINT_ID) + .with_token_program(bad_token_prog) + .build(); + + (keys, ps, inf_mint_supply, withdraw_to_balance) + }, + ) + }) + .prop_map(to_inp) +} + +proptest! { + #[test] + fn withdraw_protocol_fees_v2_wrong_token_prog_pt( + (ix, bef) in wrong_token_prog_strat(), + ) { + silence_mollusk_logs(); + SVM.with(|svm| { + withdraw_protocol_fees_v2_test(svm, ix, &bef, Some(INVALID_ARGUMENT)); + }); + } +} + +fn wrong_mint_strat() -> impl Strategy { + (0..=SAFE_MUL_U64_MAX) + .prop_flat_map(|tsv| { + ( + pool_state_v2_u64s_solvent_strat(tsv), + any_normal_pk().prop_filter("mint must not match", |pk| *pk != INF_MINT_ID), + ) + .prop_flat_map(|(solvent_u64s, wrong_mint)| { + ( + any_normal_pk(), + any_pool_state_v2(PoolStateV2FtaStrat { + u8_bools: pool_state_v2_u8_bools_normal_strat(), + addrs: PoolStateV2Addrs::default() + .with_lp_token_mint(Some(Just(wrong_mint).boxed())), + u64s: PoolStateV2U64s::default() + .with_total_sol_value(Some( + Just(*solvent_u64s.total_sol_value()).boxed(), + )) + .with_withheld_lamports(Some( + Just(*solvent_u64s.withheld_lamports()).boxed(), + )) + .with_protocol_fee_lamports(Some( + Just(*solvent_u64s.protocol_fee_lamports()).boxed(), + )) + .with_last_release_slot(Some(Just(0).boxed())), + ..Default::default() + }), + 0..=SAFE_MUL_U64_MAX, + 0..=u64::MAX, + ) + }) + .prop_map(|(wt_pk, ps, inf_mint_supply, withdraw_to_balance)| { + let keys = NewWithdrawProtocolFeesV2IxAccsBuilder::start() + .with_pool_state(POOL_STATE_ID) + .with_beneficiary(ps.protocol_fee_beneficiary) + .with_withdraw_to(wt_pk) + .with_inf_mint(INF_MINT_ID) + .with_token_program(TOKENKEG_PROGRAM) + .build(); + + (keys, ps, inf_mint_supply, withdraw_to_balance) + }) + }) + .prop_map(to_inp) +} + +proptest! { + #[test] + fn withdraw_protocol_fees_v2_wrong_mint_pt( + (ix, bef) in wrong_mint_strat(), + ) { + silence_mollusk_logs(); + SVM.with(|svm| { + withdraw_protocol_fees_v2_test(svm, ix, &bef, Some(INVALID_ARGUMENT)); + }); + } +} diff --git a/docs/v2/README.md b/docs/v2/README.md index 1508bf36..c3eafcd0 100644 --- a/docs/v2/README.md +++ b/docs/v2/README.md @@ -237,6 +237,13 @@ Same as [SwapExactInV2](#swapexactinv2), but - mints INF proportionally according to current accumulated `pool_state.protocol_fee_lamports` (should be equivalent to adding liquidity of equivalent SOL value) - reset `pool_state.protocol_fee_lamports` to 0 +###### No-op Cases + +The instruction succeeds with no state changes (no INF minted, `protocol_fee_lamports` unchanged) in the following cases: + +- No `protocol_fee_lamports` to distribute +- Accumulated `protocol_fee_lamports` is insufficient to mint any INF + ##### SetRps Set `pool_state.rps` to a new value. diff --git a/test-utils/src/gen/controller/pool_state/v2.rs b/test-utils/src/gen/controller/pool_state/v2.rs index 4b1a351a..9ac6bae3 100644 --- a/test-utils/src/gen/controller/pool_state/v2.rs +++ b/test-utils/src/gen/controller/pool_state/v2.rs @@ -10,7 +10,10 @@ use proptest::prelude::*; use solana_account::Account; use solana_pubkey::Pubkey; -use crate::{any_ctl_fee_nanos_strat, any_rps_strat, bool_strat, bool_to_u8, pk_strat, u64_strat}; +use crate::{ + any_ctl_fee_nanos_strat, any_rps_strat, bals_from_supply, bool_strat, bool_to_u8, pk_strat, + u64_strat, +}; /// If `Option::None`, `any()` is used pub type PoolStateV2FtaStrat = PoolStateV2Fta< @@ -33,6 +36,15 @@ pub fn pool_state_v2_u8_bools_normal_strat() -> PoolStateV2U8Bools impl Strategy> { + bals_from_supply::<2>(tsv).prop_map(move |([withheld, protocol_fee], _rem)| { + PoolStateV2U64s::default() + .with_total_sol_value(tsv) + .with_withheld_lamports(withheld) + .with_protocol_fee_lamports(protocol_fee) + }) +} + pub fn any_pool_state_v2( PoolStateV2FtaStrat { addrs, diff --git a/test-utils/src/keys.rs b/test-utils/src/keys.rs index c9010124..33b846b2 100644 --- a/test-utils/src/keys.rs +++ b/test-utils/src/keys.rs @@ -10,3 +10,5 @@ pub const JUPSOL_MINT: Pubkey = Pubkey::from_str_const("jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v"); pub const WSOL_MINT: Pubkey = Pubkey::from_str_const("So11111111111111111111111111111111111111112"); + +pub const INF_MINT: Pubkey = Pubkey::from_str_const("5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm");