diff --git a/Anchor.toml b/Anchor.toml index e2e2d9f2..7a97719b 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -48,6 +48,8 @@ v06-dump-daos-proposals = "yarn run tsx scripts/v0.6/dumpDaosProposals.ts" v06-migrate-daos-proposals = "yarn run tsx scripts/v0.6/migrateDaosProposals.ts" v06-create-dao = "yarn run tsx scripts/v0.6/createDao.ts" v06-provide-liquidity = "yarn run tsx scripts/v0.6/provideLiquidity.ts" +v06-collect-meteora-damm-fees = "yarn run tsx scripts/v0.6/collectMeteoraDammFees.ts" +v07-collect-meteora-damm-fees = "yarn run tsx scripts/v0.7/collectMeteoraDammFees.ts" [test] startup_wait = 5000 diff --git a/Cargo.lock b/Cargo.lock index eb64d2e6..d20734a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -942,6 +942,7 @@ dependencies = [ "anchor-lang", "anchor-spl", "conditional_vault", + "damm_v2_cpi", "solana-security-txt", "squads-multisig-program", ] diff --git a/package.json b/package.json index 535445cb..20afedfc 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@metaplex-foundation/umi-signer-wallet-adapters": "^1.1.1", "@metaplex-foundation/umi-uploader-bundlr": "^0.9.1", "@metaplex-foundation/umi-web3js-adapters": "^1.1.1", + "@meteora-ag/cp-amm-sdk": "^1.2.6", "@noble/ed25519": "^2.0.0", "@noble/secp256k1": "^2.0.0", "@solana/spl-token": "^0.3.7", diff --git a/programs/damm_v2_cpi/src/lib.rs b/programs/damm_v2_cpi/src/lib.rs index acc811e5..f8a5cf9b 100644 --- a/programs/damm_v2_cpi/src/lib.rs +++ b/programs/damm_v2_cpi/src/lib.rs @@ -30,6 +30,10 @@ pub mod damm_v2_cpi { ) -> Result<()> { Ok(()) } + + pub fn claim_position_fee(_ctx: Context) -> Result<()> { + Ok(()) + } } /// Information regarding fee charges @@ -151,3 +155,60 @@ pub struct InitializePoolWithDynamicConfigCtx<'info> { // Sysvar for program account pub system_program: Program<'info, System>, } + +#[event_cpi] +#[derive(Accounts)] +pub struct ClaimPositionFeeCtx<'info> { + /// CHECK: pool authority + pub pool_authority: UncheckedAccount<'info>, + + /// CHECK: CPI + pub pool: UncheckedAccount<'info>, + + /// CHECK: CPI + #[account(mut)] + pub position: UncheckedAccount<'info>, + + /// The user token a account + /// CHECK: CPI + #[account(mut)] + pub token_a_account: UncheckedAccount<'info>, + + /// The user token b account + /// CHECK: CPI + #[account(mut)] + pub token_b_account: UncheckedAccount<'info>, + + /// The vault token account for input token + /// CHECK: CPI + #[account(mut)] + pub token_a_vault: UncheckedAccount<'info>, + + /// The vault token account for output token + /// CHECK: CPI + #[account(mut)] + pub token_b_vault: UncheckedAccount<'info>, + + /// The mint of token a + /// CHECK: CPI + pub token_a_mint: UncheckedAccount<'info>, + + /// The mint of token b + /// CHECK: CPI + pub token_b_mint: UncheckedAccount<'info>, + + /// The token account for nft + /// CHECK: CPI + pub position_nft_account: UncheckedAccount<'info>, + + /// owner of position + pub owner: Signer<'info>, + + /// Token a program + /// CHECK: CPI + pub token_a_program: UncheckedAccount<'info>, + + /// Token b program + /// CHECK: CPI + pub token_b_program: UncheckedAccount<'info>, +} diff --git a/programs/futarchy/Cargo.toml b/programs/futarchy/Cargo.toml index d543d36a..9e9d4476 100644 --- a/programs/futarchy/Cargo.toml +++ b/programs/futarchy/Cargo.toml @@ -22,3 +22,4 @@ anchor-spl = "^0.29.0" solana-security-txt = "1.1.1" conditional_vault = { path = "../conditional_vault", features = ["cpi"] } squads-multisig-program = { git = "https://github.com/Squads-Protocol/v4", package = "squads-multisig-program", rev = "6d5235da621a2e9b7379ea358e48760e981053be", features = ["cpi"] } +damm_v2_cpi = { path = "../damm_v2_cpi", features = ["cpi"] } diff --git a/programs/futarchy/src/error.rs b/programs/futarchy/src/error.rs index 16725002..e40223c5 100644 --- a/programs/futarchy/src/error.rs +++ b/programs/futarchy/src/error.rs @@ -74,4 +74,6 @@ pub enum FutarchyError { InvalidTeamSponsoredPassThreshold, #[msg("Target K must be greater than the current K")] InvalidTargetK, + #[msg("Failed to compile transaction message for Squads vault transaction")] + InvalidTransactionMessage, } diff --git a/programs/futarchy/src/events.rs b/programs/futarchy/src/events.rs index e40c9e8d..5e96aedf 100644 --- a/programs/futarchy/src/events.rs +++ b/programs/futarchy/src/events.rs @@ -190,3 +190,16 @@ pub struct SponsorProposalEvent { pub dao: Pubkey, pub team_address: Pubkey, } + +#[event] +pub struct CollectMeteoraDammFeesEvent { + pub common: CommonFields, + pub dao: Pubkey, + pub pool: Pubkey, + pub base_token_account: Pubkey, + pub quote_token_account: Pubkey, + pub quote_mint: Pubkey, + pub base_mint: Pubkey, + pub quote_fees_collected: u64, + pub base_fees_collected: u64, +} diff --git a/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs b/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs new file mode 100644 index 00000000..d068f5e2 --- /dev/null +++ b/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs @@ -0,0 +1,503 @@ +use anchor_lang::AnchorSerialize; +use anchor_lang::InstructionData; +use damm_v2_cpi::program::DammV2Cpi; +use std::collections::BTreeMap; + +use super::*; + +pub mod metadao_multisig_vault { + use anchor_lang::prelude::declare_id; + + // MetaDAO multisig + declare_id!("6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf"); +} + +pub mod metadao_admin { + use anchor_lang::prelude::declare_id; + + // We must use a non-Squads signer because of CPI depth limits + declare_id!("tSTp6B6kE9o6ZaTmHm2ZwnJBBtgd3x112tapxFhmBEQ"); +} + +pub mod pool_authority { + use anchor_lang::prelude::declare_id; + + // DAMM V2 Pool Authority + declare_id!("HLnpSz9h2S4hiLQ43rnSD9XkcUThA7B8hQMKmDaiTLcC"); +} + +#[derive(Accounts)] +#[event_cpi] +pub struct CollectMeteoraDammFees<'info> { + #[account( + mut, + constraint = dao.base_mint == meteora_claim_position_fees_accounts.token_a_mint.key(), + constraint = dao.quote_mint == meteora_claim_position_fees_accounts.token_b_mint.key() + )] + pub dao: Account<'info, Dao>, + #[account(mut)] + pub admin: Signer<'info>, + + /// CHECK: checked by autocrat program + #[account(mut, seeds = [squads_multisig_program::SEED_PREFIX, squads_multisig_program::SEED_MULTISIG, dao.key().as_ref()], bump, seeds::program = squads_program)] + pub squads_multisig: Account<'info, squads_multisig_program::Multisig>, + /// CHECK: signer for the squads transaction, checked by squads program + #[account(seeds = [squads_multisig_program::SEED_PREFIX, squads_multisig.key().as_ref(), squads_multisig_program::SEED_VAULT, 0_u8.to_le_bytes().as_ref()], bump, seeds::program = squads_program)] + pub squads_multisig_vault: UncheckedAccount<'info>, + /// CHECK: squads transaction, initialized by squads multisig program, checked by squads multisig program + #[account(mut)] + pub squads_multisig_vault_transaction: UncheckedAccount<'info>, + /// CHECK: squads proposal, initialized by squads multisig program, checked by squads multisig program + #[account(mut)] + pub squads_multisig_proposal: UncheckedAccount<'info>, + + #[account(address = permissionless_account::id())] + pub squads_multisig_permissionless_account: Signer<'info>, + + pub meteora_claim_position_fees_accounts: MeteoraClaimPositionFeesAccounts<'info>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, + pub squads_program: Program<'info, squads_multisig_program::program::SquadsMultisigProgram>, +} + +#[derive(Accounts)] +pub struct MeteoraClaimPositionFeesAccounts<'info> { + pub damm_v2_program: Program<'info, DammV2Cpi>, + + /// CHECK: checked by damm v2 program + pub damm_v2_event_authority: UncheckedAccount<'info>, + + /// CHECK: checked by damm v2 program + #[account(address = pool_authority::ID)] + pub pool_authority: UncheckedAccount<'info>, + + /// CHECK: checked by damm v2 program + pub pool: UncheckedAccount<'info>, + + /// CHECK: checked by damm v2 program + #[account(mut)] + pub position: UncheckedAccount<'info>, + + /// Token account of base tokens recipient + #[account(mut, associated_token::mint = token_a_mint, associated_token::authority = metadao_multisig_vault::ID)] + pub token_a_account: Account<'info, TokenAccount>, + + /// Token account of quote tokens recipient + #[account(mut, associated_token::mint = token_b_mint, associated_token::authority = metadao_multisig_vault::ID)] + pub token_b_account: Account<'info, TokenAccount>, + + /// CHECK: checked by damm v2 program, base token vault of the pool + #[account(mut)] + pub token_a_vault: UncheckedAccount<'info>, + + /// CHECK: checked by damm v2 program, quote token vault of the pool + #[account(mut)] + pub token_b_vault: UncheckedAccount<'info>, + + /// CHECK: Checked from dao struct, base mint + pub token_a_mint: UncheckedAccount<'info>, + + /// CHECK: Checked from dao struct, quote mint + pub token_b_mint: UncheckedAccount<'info>, + + /// CHECK: checked by damm v2 program + pub position_nft_account: UncheckedAccount<'info>, + + /// CHECK: checked by damm v2 program, owner of position - usually the DAO's squads multisig vault + pub owner: UncheckedAccount<'info>, + + /// CHECK: checked by damm v2 program, base token program + pub token_a_program: UncheckedAccount<'info>, + + /// CHECK: checked by damm v2 program, quote token program + pub token_b_program: UncheckedAccount<'info>, +} + +impl CollectMeteoraDammFees<'_> { + pub fn validate(&self) -> Result<()> { + #[cfg(feature = "production")] + require_keys_eq!( + self.admin.key(), + metadao_admin::ID, + FutarchyError::InvalidAdmin + ); + + Ok(()) + } + + pub fn handle(ctx: Context) -> Result<()> { + let ix_data = damm_v2_cpi::instruction::ClaimPositionFee {}.data(); + + // Record token balances before the fee claim + let base_token_account_balance_before = ctx + .accounts + .meteora_claim_position_fees_accounts + .token_a_account + .amount; + let quote_token_account_balance_before = ctx + .accounts + .meteora_claim_position_fees_accounts + .token_b_account + .amount; + + let account_infos = damm_v2_cpi::cpi::accounts::ClaimPositionFeeCtx { + pool_authority: ctx + .accounts + .meteora_claim_position_fees_accounts + .pool_authority + .to_account_info(), + pool: ctx + .accounts + .meteora_claim_position_fees_accounts + .pool + .to_account_info(), + position: ctx + .accounts + .meteora_claim_position_fees_accounts + .position + .to_account_info(), + token_a_account: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_a_account + .to_account_info(), + token_b_account: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_b_account + .to_account_info(), + token_a_vault: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_a_vault + .to_account_info(), + token_b_vault: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_b_vault + .to_account_info(), + token_a_mint: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_a_mint + .to_account_info(), + token_b_mint: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_b_mint + .to_account_info(), + position_nft_account: ctx + .accounts + .meteora_claim_position_fees_accounts + .position_nft_account + .to_account_info(), + owner: ctx + .accounts + .meteora_claim_position_fees_accounts + .owner + .to_account_info(), + token_a_program: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_a_program + .to_account_info(), + token_b_program: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_b_program + .to_account_info(), + event_authority: ctx + .accounts + .meteora_claim_position_fees_accounts + .damm_v2_event_authority + .to_account_info(), + program: ctx + .accounts + .meteora_claim_position_fees_accounts + .damm_v2_program + .to_account_info(), + }; + + let accounts = account_infos.to_account_metas(None); + + let ix = anchor_lang::solana_program::instruction::Instruction { + program_id: damm_v2_cpi::ID, + accounts, + data: ix_data, + }; + + // Compile the transaction message in Squads' format + // This correctly sets num_writable_signers and num_writable_non_signers + // instead of the inverted readonly counts from Solana's Message::serialize() + let transaction_message = + compile_transaction_message(&ctx.accounts.squads_multisig_vault.key(), &[ix])?; + + let transaction_message_bytes = transaction_message.try_to_vec()?; + + let dao_nonce = &ctx.accounts.dao.nonce.to_le_bytes(); + let dao_creator_key = ctx.accounts.dao.dao_creator.as_ref(); + let dao_seeds = &[ + b"dao".as_ref(), + dao_creator_key, + dao_nonce, + &[ctx.accounts.dao.pda_bump], + ]; + + let dao_signer = &[&dao_seeds[..]]; + + squads_multisig_program::cpi::vault_transaction_create( + CpiContext::new( + ctx.accounts.squads_program.to_account_info(), + squads_multisig_program::cpi::accounts::VaultTransactionCreate { + creator: ctx + .accounts + .squads_multisig_permissionless_account + .to_account_info(), + multisig: ctx.accounts.squads_multisig.to_account_info(), + rent_payer: ctx.accounts.admin.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + transaction: ctx + .accounts + .squads_multisig_vault_transaction + .to_account_info(), + }, + ), + squads_multisig_program::VaultTransactionCreateArgs { + ephemeral_signers: 0, + vault_index: 0, + transaction_message: transaction_message_bytes, + memo: None, + }, + )?; + + // Reload the squads multisig account to get the latest transaction index + ctx.accounts.squads_multisig.reload()?; + let transaction_index = ctx.accounts.squads_multisig.transaction_index; + + squads_multisig_program::cpi::proposal_create( + CpiContext::new_with_signer( + ctx.accounts.squads_program.to_account_info(), + squads_multisig_program::cpi::accounts::ProposalCreate { + // DAO is the config authority - maybe this needs to be the permissionless account instead? + creator: ctx.accounts.dao.to_account_info(), + multisig: ctx.accounts.squads_multisig.to_account_info(), + rent_payer: ctx.accounts.admin.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + proposal: ctx.accounts.squads_multisig_proposal.to_account_info(), + }, + dao_signer, + ), + squads_multisig_program::ProposalCreateArgs { + transaction_index, + draft: false, + }, + )?; + + squads_multisig_program::cpi::proposal_approve( + CpiContext::new_with_signer( + ctx.accounts.squads_program.to_account_info(), + squads_multisig_program::cpi::accounts::ProposalVote { + proposal: ctx.accounts.squads_multisig_proposal.to_account_info(), + multisig: ctx.accounts.squads_multisig.to_account_info(), + member: ctx.accounts.dao.to_account_info(), // DAO is the config authority + }, + dao_signer, + ), + squads_multisig_program::ProposalVoteArgs { memo: None }, + )?; + + let transaction = squads_multisig_program::state::VaultTransaction::try_from_slice( + &ctx.accounts + .squads_multisig_vault_transaction + .to_account_info() + .try_borrow_data() + .unwrap() + .as_ref()[8..], + )?; + + let all_accounts = ctx.accounts.to_account_infos(); + + let remaining_accounts = transaction + .message + .account_keys + .iter() + .map(|key| { + all_accounts + .iter() + .find(|account| account.key() == *key) + .map(|account| account.to_account_info()) + .unwrap() + }) + .collect::>>(); + + squads_multisig_program::cpi::vault_transaction_execute( + CpiContext::new_with_signer( + ctx.accounts.squads_program.to_account_info(), + squads_multisig_program::cpi::accounts::VaultTransactionExecute { + multisig: ctx.accounts.squads_multisig.to_account_info(), + proposal: ctx.accounts.squads_multisig_proposal.to_account_info(), + transaction: ctx + .accounts + .squads_multisig_vault_transaction + .to_account_info(), + member: ctx.accounts.dao.to_account_info(), + }, + dao_signer, + ) + .with_remaining_accounts(remaining_accounts), + )?; + + // Reload token accounts and record token balances after the fee claim + ctx.accounts + .meteora_claim_position_fees_accounts + .token_a_account + .reload()?; + ctx.accounts + .meteora_claim_position_fees_accounts + .token_b_account + .reload()?; + + let base_token_account_balance_after = ctx + .accounts + .meteora_claim_position_fees_accounts + .token_a_account + .amount; + let quote_token_account_balance_after = ctx + .accounts + .meteora_claim_position_fees_accounts + .token_b_account + .amount; + + let base_fees_collected = + base_token_account_balance_after - base_token_account_balance_before; + let quote_fees_collected = + quote_token_account_balance_after - quote_token_account_balance_before; + + ctx.accounts.dao.seq_num += 1; + + emit_cpi!(CollectMeteoraDammFeesEvent { + common: CommonFields::new(&Clock::get()?, ctx.accounts.dao.seq_num), + dao: ctx.accounts.dao.key(), + base_token_account: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_a_account + .key(), + quote_token_account: ctx + .accounts + .meteora_claim_position_fees_accounts + .token_b_account + .key(), + pool: ctx.accounts.meteora_claim_position_fees_accounts.pool.key(), + quote_mint: ctx.accounts.dao.quote_mint, + base_mint: ctx.accounts.dao.base_mint, + quote_fees_collected: quote_fees_collected, + base_fees_collected: base_fees_collected, + }); + + Ok(()) + } +} + +/// Compiles a Solana instruction into a Squads TransactionMessage format. +/// This is necessary because Solana's Message::serialize() uses a different header format +/// (num_readonly_signed_accounts, num_readonly_unsigned_accounts) than Squads expects +/// (num_writable_signers, num_writable_non_signers). +fn compile_transaction_message( + vault_key: &Pubkey, + instructions: &[anchor_lang::solana_program::instruction::Instruction], +) -> Result { + // Track account metadata: (is_signer, is_writable) + let mut key_meta_map: BTreeMap = BTreeMap::new(); + + // Add vault as a signer (it will sign the vault transaction) + // Writability is determined by whether it appears as writable in instruction accounts + key_meta_map.insert(*vault_key, (true, false)); + + // Collect all accounts from instructions, merging their flags with OR + for ix in instructions { + // Program ID is a non-signer, non-writable account + key_meta_map.entry(ix.program_id).or_insert((false, false)); + + for meta in &ix.accounts { + let entry = key_meta_map.entry(meta.pubkey).or_insert((false, false)); + entry.0 |= meta.is_signer; + entry.1 |= meta.is_writable; + } + } + + // Sort accounts into: writable signers, readonly signers, writable non-signers, readonly non-signers + let mut writable_signers: Vec = Vec::new(); + let mut readonly_signers: Vec = Vec::new(); + let mut writable_non_signers: Vec = Vec::new(); + let mut readonly_non_signers: Vec = Vec::new(); + + for (pubkey, (is_signer, is_writable)) in &key_meta_map { + if *is_signer && *is_writable { + writable_signers.push(*pubkey); + } else if *is_signer { + // Vault key should be first among readonly signers + if *pubkey == *vault_key { + readonly_signers.insert(0, *pubkey); + } else { + readonly_signers.push(*pubkey); + } + } else if *is_writable { + writable_non_signers.push(*pubkey); + } else { + readonly_non_signers.push(*pubkey); + } + } + + // Build the final account keys list in sorted order + let mut account_keys: Vec = Vec::new(); + account_keys.extend(&writable_signers); + account_keys.extend(&readonly_signers); + account_keys.extend(&writable_non_signers); + account_keys.extend(&readonly_non_signers); + + // Calculate counts + let num_signers = (writable_signers.len() + readonly_signers.len()) as u8; + let num_writable_signers = writable_signers.len() as u8; + let num_writable_non_signers = writable_non_signers.len() as u8; + + // Build account key index lookup + let key_to_index: BTreeMap = account_keys + .iter() + .enumerate() + .map(|(i, k)| (*k, i as u8)) + .collect(); + + // Compile instructions with new indices + let mut compiled_instructions: Vec = Vec::new(); + for ix in instructions { + let program_id_index = *key_to_index + .get(&ix.program_id) + .ok_or(FutarchyError::InvalidTransactionMessage)?; + + let account_indexes: Vec = ix + .accounts + .iter() + .map(|meta| key_to_index.get(&meta.pubkey).copied()) + .collect::>>() + .ok_or(FutarchyError::InvalidTransactionMessage)?; + + compiled_instructions.push(squads_multisig_program::CompiledInstruction { + program_id_index, + account_indexes: squads_multisig_program::SmallVec::from(account_indexes), + data: squads_multisig_program::SmallVec::from(ix.data.clone()), + }); + } + + Ok(squads_multisig_program::TransactionMessage { + num_signers, + num_writable_signers, + num_writable_non_signers, + account_keys: squads_multisig_program::SmallVec::from(account_keys), + instructions: squads_multisig_program::SmallVec::from(compiled_instructions), + address_table_lookups: squads_multisig_program::SmallVec::from(Vec::< + squads_multisig_program::MessageAddressTableLookup, + >::new()), + }) +} diff --git a/programs/futarchy/src/instructions/mod.rs b/programs/futarchy/src/instructions/mod.rs index 356267fb..fc9ab44b 100644 --- a/programs/futarchy/src/instructions/mod.rs +++ b/programs/futarchy/src/instructions/mod.rs @@ -1,6 +1,7 @@ use super::*; pub mod collect_fees; +pub mod collect_meteora_damm_fees; pub mod conditional_swap; pub mod execute_spending_limit_change; pub mod finalize_proposal; @@ -18,6 +19,7 @@ pub mod update_dao; pub mod withdraw_liquidity; pub use collect_fees::*; +pub use collect_meteora_damm_fees::*; pub use conditional_swap::*; pub use execute_spending_limit_change::*; pub use finalize_proposal::*; diff --git a/programs/futarchy/src/lib.rs b/programs/futarchy/src/lib.rs index 5f16cd79..34273f7a 100644 --- a/programs/futarchy/src/lib.rs +++ b/programs/futarchy/src/lib.rs @@ -143,6 +143,11 @@ pub mod futarchy { SponsorProposal::handle(ctx) } + #[access_control(ctx.accounts.validate())] + pub fn collect_meteora_damm_fees(ctx: Context) -> Result<()> { + CollectMeteoraDammFees::handle(ctx) + } + pub fn resize_dao(ctx: Context) -> Result<()> { ResizeDao::handle(ctx) } diff --git a/scripts/v0.6/collectMeteoraDammFees.ts b/scripts/v0.6/collectMeteoraDammFees.ts new file mode 100644 index 00000000..df9128a4 --- /dev/null +++ b/scripts/v0.6/collectMeteoraDammFees.ts @@ -0,0 +1,53 @@ +import * as anchor from "@coral-xyz/anchor"; +import * as multisig from "@sqds/multisig"; +import { + CONDITIONAL_VAULT_PROGRAM_ID, + FUTARCHY_PROGRAM_ID, + FutarchyClient, + MAINNET_METEORA_CONFIG as V0_6_MAINNET_METEORA_CONFIG, +} from "@metadaoproject/futarchy/v0.6"; +import { PublicKey } from "@solana/web3.js"; + +const provider = anchor.AnchorProvider.env(); + +// Payer MUST be the non-Squads signer - tSTp6B6kE9o6ZaTmHm2ZwnJBBtgd3x112tapxFhmBEQ +const payer = provider.wallet["payer"]; + +const futarchy: FutarchyClient = new FutarchyClient( + provider, + FUTARCHY_PROGRAM_ID, + CONDITIONAL_VAULT_PROGRAM_ID, + [], +); + +// Set the DAO address before running the script +const dao = new PublicKey(""); + +export const collectMeteoraDammFees = async () => { + const daoAccount = await futarchy.fetchDao(dao); + + const squadsMultisigAccount = + await multisig.accounts.Multisig.fromAccountAddress( + provider.connection, + daoAccount.squadsMultisig, + ); + + const collectMeteoraDammFeesIx = await futarchy + .collectMeteoraDammFeesIx({ + dao, + baseMint: daoAccount.baseMint, + quoteMint: daoAccount.quoteMint, + transactionIndex: + BigInt(squadsMultisigAccount.transactionIndex.toString()) + 1n, + meteoraConfig: V0_6_MAINNET_METEORA_CONFIG, + }) + .signers([payer]) + .rpc(); + + console.log( + "Collect Meteora DAMM fees transaction:", + collectMeteoraDammFeesIx, + ); +}; + +collectMeteoraDammFees().catch(console.error); diff --git a/scripts/v0.7/collectMeteoraDammFees.ts b/scripts/v0.7/collectMeteoraDammFees.ts new file mode 100644 index 00000000..ad00aaee --- /dev/null +++ b/scripts/v0.7/collectMeteoraDammFees.ts @@ -0,0 +1,53 @@ +import * as anchor from "@coral-xyz/anchor"; +import * as multisig from "@sqds/multisig"; +import { + CONDITIONAL_VAULT_PROGRAM_ID, + FUTARCHY_PROGRAM_ID, + FutarchyClient, + MAINNET_METEORA_CONFIG as V0_7_MAINNET_METEORA_CONFIG, +} from "@metadaoproject/futarchy/v0.7"; +import { PublicKey } from "@solana/web3.js"; + +const provider = anchor.AnchorProvider.env(); + +// Payer MUST be the non-Squads signer - tSTp6B6kE9o6ZaTmHm2ZwnJBBtgd3x112tapxFhmBEQ +const payer = provider.wallet["payer"]; + +const futarchy: FutarchyClient = new FutarchyClient( + provider, + FUTARCHY_PROGRAM_ID, + CONDITIONAL_VAULT_PROGRAM_ID, + [], +); + +// Set the DAO address before running the script +const dao = new PublicKey(""); + +export const collectMeteoraDammFees = async () => { + const daoAccount = await futarchy.fetchDao(dao); + + const squadsMultisigAccount = + await multisig.accounts.Multisig.fromAccountAddress( + provider.connection, + daoAccount.squadsMultisig, + ); + + const collectMeteoraDammFeesIx = await futarchy + .collectMeteoraDammFeesIx({ + dao, + baseMint: daoAccount.baseMint, + quoteMint: daoAccount.quoteMint, + transactionIndex: + BigInt(squadsMultisigAccount.transactionIndex.toString()) + 1n, + meteoraConfig: V0_7_MAINNET_METEORA_CONFIG, + }) + .signers([payer]) + .rpc(); + + console.log( + "Collect Meteora DAMM fees transaction:", + collectMeteoraDammFeesIx, + ); +}; + +collectMeteoraDammFees().catch(console.error); diff --git a/sdk/src/v0.6/FutarchyClient.ts b/sdk/src/v0.6/FutarchyClient.ts index ff4fade0..ed327d10 100644 --- a/sdk/src/v0.6/FutarchyClient.ts +++ b/sdk/src/v0.6/FutarchyClient.ts @@ -36,6 +36,11 @@ import { SQUADS_PROGRAM_ID, USDC_DECIMALS, SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + DAMM_V2_PROGRAM_ID, + DAMM_V2_POOL_AUTHORITY, + MAINNET_METEORA_CONFIG, + LAUNCHPAD_PROGRAM_ID, + METADAO_MULTISIG_VAULT, } from "./constants.js"; import { DEFAULT_CU_PRICE, @@ -999,4 +1004,125 @@ export class FutarchyClient { teamAddress, }); } + + collectMeteoraDammFeesIx({ + dao, + baseMint, + quoteMint = MAINNET_USDC, + transactionIndex, + meteoraConfig = MAINNET_METEORA_CONFIG, + admin = this.provider.publicKey, + }: { + dao: PublicKey; + baseMint: PublicKey; + quoteMint?: PublicKey; + transactionIndex: bigint; + meteoraConfig?: PublicKey; + admin?: PublicKey; + }) { + // Squads accounts + const multisigPda = multisig.getMultisigPda({ createKey: dao })[0]; + const squadsMultisigVault = multisig.getVaultPda({ + multisigPda, + index: 0, + })[0]; + const squadsMultisigVaultTransaction = multisig.getTransactionPda({ + multisigPda, + index: transactionIndex, + })[0]; + const squadsMultisigProposal = multisig.getProposalPda({ + multisigPda, + transactionIndex, + })[0]; + + // Token accounts for receiving fees + const baseTokenAccount = getAssociatedTokenAddressSync( + baseMint, + METADAO_MULTISIG_VAULT, + true, + ); + const quoteTokenAccount = getAssociatedTokenAddressSync( + quoteMint, + METADAO_MULTISIG_VAULT, + true, + ); + + // Helper function to sort mints for Meteora pool PDA + const sortMints = ( + mint1: PublicKey, + mint2: PublicKey, + ): [Buffer, Buffer] => { + const buf1 = mint1.toBuffer(); + const buf2 = mint2.toBuffer(); + if (Buffer.compare(buf1, buf2) > 0) { + return [buf1, buf2]; + } + return [buf2, buf1]; + }; + + const [sortedMint1, sortedMint2] = sortMints(baseMint, quoteMint); + + // Meteora DAMM accounts + const [pool] = PublicKey.findProgramAddressSync( + [Buffer.from("pool"), meteoraConfig.toBuffer(), sortedMint1, sortedMint2], + DAMM_V2_PROGRAM_ID, + ); + + const [positionNftMint] = PublicKey.findProgramAddressSync( + [Buffer.from("position_nft_mint"), baseMint.toBuffer()], + LAUNCHPAD_PROGRAM_ID, + ); + + const [positionNftAccount] = PublicKey.findProgramAddressSync( + [Buffer.from("position_nft_account"), positionNftMint.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const [position] = PublicKey.findProgramAddressSync( + [Buffer.from("position"), positionNftMint.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const [tokenAVault] = PublicKey.findProgramAddressSync( + [Buffer.from("token_vault"), baseMint.toBuffer(), pool.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const [tokenBVault] = PublicKey.findProgramAddressSync( + [Buffer.from("token_vault"), quoteMint.toBuffer(), pool.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const [dammV2EventAuthority] = getEventAuthorityAddr(DAMM_V2_PROGRAM_ID); + + return this.futarchy.methods.collectMeteoraDammFees().accounts({ + dao, + admin, + squadsMultisig: multisigPda, + squadsMultisigVault, + squadsMultisigVaultTransaction, + squadsMultisigProposal, + squadsMultisigPermissionlessAccount: PERMISSIONLESS_ACCOUNT.publicKey, + meteoraClaimPositionFeesAccounts: { + dammV2Program: DAMM_V2_PROGRAM_ID, + dammV2EventAuthority, + poolAuthority: DAMM_V2_POOL_AUTHORITY, + pool, + position, + tokenAAccount: baseTokenAccount, + tokenBAccount: quoteTokenAccount, + tokenAVault, + tokenBVault, + tokenAMint: baseMint, + tokenBMint: quoteMint, + positionNftAccount, + owner: squadsMultisigVault, + tokenAProgram: TOKEN_PROGRAM_ID, + tokenBProgram: TOKEN_PROGRAM_ID, + }, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + squadsProgram: SQUADS_PROGRAM_ID, + }); + } } diff --git a/sdk/src/v0.6/constants.ts b/sdk/src/v0.6/constants.ts index 55e176c7..9466cb05 100644 --- a/sdk/src/v0.6/constants.ts +++ b/sdk/src/v0.6/constants.ts @@ -65,6 +65,10 @@ export const DAMM_V2_PROGRAM_ID = new PublicKey( "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG", ); +export const DAMM_V2_POOL_AUTHORITY = new PublicKey( + "HLnpSz9h2S4hiLQ43rnSD9XkcUThA7B8hQMKmDaiTLcC", +); + export const LOW_FEE_RAYDIUM_CONFIG = new PublicKey( "D4FPEruKEHrG5TenZ2mpDGEfu1iUvTiqBxvpU8HLBvC2", ); @@ -105,6 +109,10 @@ export const MAINNET_METEORA_CONFIG = new PublicKey( "Asv1KQqeop9e4FFvTzEBZhwtTjuWHXPq5thUGtQrzzA3", ); +export const METADAO_MULTISIG_VAULT = new PublicKey( + "6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf", +); + export const PERMISSIONLESS_ACCOUNT = Keypair.fromSecretKey( Uint8Array.from([ 249, 158, 188, 171, 243, 143, 1, 48, 87, 243, 209, 153, 144, 106, 23, 88, diff --git a/sdk/src/v0.6/types/damm_v2_cpi.ts b/sdk/src/v0.6/types/damm_v2_cpi.ts index e9e83a82..44882927 100644 --- a/sdk/src/v0.6/types/damm_v2_cpi.ts +++ b/sdk/src/v0.6/types/damm_v2_cpi.ts @@ -121,6 +121,97 @@ export type DammV2Cpi = { }, ]; }, + { + name: "claimPositionFee"; + accounts: [ + { + name: "poolAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "pool"; + isMut: false; + isSigner: false; + }, + { + name: "position"; + isMut: true; + isSigner: false; + }, + { + name: "tokenAAccount"; + isMut: true; + isSigner: false; + docs: ["The user token a account"]; + }, + { + name: "tokenBAccount"; + isMut: true; + isSigner: false; + docs: ["The user token b account"]; + }, + { + name: "tokenAVault"; + isMut: true; + isSigner: false; + docs: ["The vault token account for input token"]; + }, + { + name: "tokenBVault"; + isMut: true; + isSigner: false; + docs: ["The vault token account for output token"]; + }, + { + name: "tokenAMint"; + isMut: false; + isSigner: false; + docs: ["The mint of token a"]; + }, + { + name: "tokenBMint"; + isMut: false; + isSigner: false; + docs: ["The mint of token b"]; + }, + { + name: "positionNftAccount"; + isMut: false; + isSigner: false; + docs: ["The token account for nft"]; + }, + { + name: "owner"; + isMut: false; + isSigner: true; + docs: ["owner of position"]; + }, + { + name: "tokenAProgram"; + isMut: false; + isSigner: false; + docs: ["Token a program"]; + }, + { + name: "tokenBProgram"; + isMut: false; + isSigner: false; + docs: ["Token b program"]; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + }, + ]; + args: []; + }, ]; types: [ { @@ -404,6 +495,97 @@ export const IDL: DammV2Cpi = { }, ], }, + { + name: "claimPositionFee", + accounts: [ + { + name: "poolAuthority", + isMut: false, + isSigner: false, + }, + { + name: "pool", + isMut: false, + isSigner: false, + }, + { + name: "position", + isMut: true, + isSigner: false, + }, + { + name: "tokenAAccount", + isMut: true, + isSigner: false, + docs: ["The user token a account"], + }, + { + name: "tokenBAccount", + isMut: true, + isSigner: false, + docs: ["The user token b account"], + }, + { + name: "tokenAVault", + isMut: true, + isSigner: false, + docs: ["The vault token account for input token"], + }, + { + name: "tokenBVault", + isMut: true, + isSigner: false, + docs: ["The vault token account for output token"], + }, + { + name: "tokenAMint", + isMut: false, + isSigner: false, + docs: ["The mint of token a"], + }, + { + name: "tokenBMint", + isMut: false, + isSigner: false, + docs: ["The mint of token b"], + }, + { + name: "positionNftAccount", + isMut: false, + isSigner: false, + docs: ["The token account for nft"], + }, + { + name: "owner", + isMut: false, + isSigner: true, + docs: ["owner of position"], + }, + { + name: "tokenAProgram", + isMut: false, + isSigner: false, + docs: ["Token a program"], + }, + { + name: "tokenBProgram", + isMut: false, + isSigner: false, + docs: ["Token b program"], + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, ], types: [ { diff --git a/sdk/src/v0.6/types/futarchy.ts b/sdk/src/v0.6/types/futarchy.ts index 30b43eb4..90068312 100644 --- a/sdk/src/v0.6/types/futarchy.ts +++ b/sdk/src/v0.6/types/futarchy.ts @@ -946,40 +946,35 @@ export type Futarchy = { args: []; }, { - name: "collectLpFees"; + name: "executeSpendingLimitChange"; accounts: [ { - name: "dao"; + name: "proposal"; isMut: true; isSigner: false; }, { - name: "admin"; - isMut: false; - isSigner: true; - }, - { - name: "baseTokenAccount"; + name: "dao"; isMut: true; isSigner: false; }, { - name: "quoteTokenAccount"; + name: "squadsProposal"; isMut: true; isSigner: false; }, { - name: "ammBaseVault"; - isMut: true; + name: "squadsMultisig"; + isMut: false; isSigner: false; }, { - name: "ammQuoteVault"; - isMut: true; + name: "squadsMultisigProgram"; + isMut: false; isSigner: false; }, { - name: "tokenProgram"; + name: "vaultTransaction"; isMut: false; isSigner: false; }, @@ -994,17 +989,10 @@ export type Futarchy = { isSigner: false; }, ]; - args: [ - { - name: "args"; - type: { - defined: "CollectLpFeesArgs"; - }; - }, - ]; + args: []; }, { - name: "executeSpendingLimitChange"; + name: "sponsorProposal"; accounts: [ { name: "proposal"; @@ -1017,56 +1005,158 @@ export type Futarchy = { isSigner: false; }, { - name: "squadsProposal"; - isMut: true; - isSigner: false; + name: "teamAddress"; + isMut: false; + isSigner: true; }, { - name: "squadsMultisig"; + name: "eventAuthority"; isMut: false; isSigner: false; }, { - name: "squadsMultisigProgram"; + name: "program"; isMut: false; isSigner: false; }, + ]; + args: []; + }, + { + name: "collectMeteoraDammFees"; + accounts: [ { - name: "vaultTransaction"; - isMut: false; + name: "dao"; + isMut: true; isSigner: false; }, { - name: "eventAuthority"; - isMut: false; + name: "admin"; + isMut: true; + isSigner: true; + }, + { + name: "squadsMultisig"; + isMut: true; isSigner: false; }, { - name: "program"; + name: "squadsMultisigVault"; isMut: false; isSigner: false; }, - ]; - args: []; - }, - { - name: "sponsorProposal"; - accounts: [ { - name: "proposal"; + name: "squadsMultisigVaultTransaction"; isMut: true; isSigner: false; }, { - name: "dao"; + name: "squadsMultisigProposal"; isMut: true; isSigner: false; }, { - name: "teamAddress"; + name: "squadsMultisigPermissionlessAccount"; isMut: false; isSigner: true; }, + { + name: "meteoraClaimPositionFeesAccounts"; + accounts: [ + { + name: "dammV2Program"; + isMut: false; + isSigner: false; + }, + { + name: "dammV2EventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "poolAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "pool"; + isMut: false; + isSigner: false; + }, + { + name: "position"; + isMut: true; + isSigner: false; + }, + { + name: "tokenAAccount"; + isMut: true; + isSigner: false; + docs: ["Token account of base tokens recipient"]; + }, + { + name: "tokenBAccount"; + isMut: true; + isSigner: false; + docs: ["Token account of quote tokens recipient"]; + }, + { + name: "tokenAVault"; + isMut: true; + isSigner: false; + }, + { + name: "tokenBVault"; + isMut: true; + isSigner: false; + }, + { + name: "tokenAMint"; + isMut: false; + isSigner: false; + }, + { + name: "tokenBMint"; + isMut: false; + isSigner: false; + }, + { + name: "positionNftAccount"; + isMut: false; + isSigner: false; + }, + { + name: "owner"; + isMut: false; + isSigner: false; + }, + { + name: "tokenAProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenBProgram"; + isMut: false; + isSigner: false; + }, + ]; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "squadsProgram"; + isMut: false; + isSigner: false; + }, { name: "eventAuthority"; isMut: false; @@ -1400,18 +1490,6 @@ export type Futarchy = { ]; }; }, - { - name: "CollectLpFeesArgs"; - type: { - kind: "struct"; - fields: [ - { - name: "targetK"; - type: "u128"; - }, - ]; - }; - }, { name: "ConditionalSwapParams"; type: { @@ -2814,6 +2892,58 @@ export type Futarchy = { }, ]; }, + { + name: "CollectMeteoraDammFeesEvent"; + fields: [ + { + name: "common"; + type: { + defined: "CommonFields"; + }; + index: false; + }, + { + name: "dao"; + type: "publicKey"; + index: false; + }, + { + name: "pool"; + type: "publicKey"; + index: false; + }, + { + name: "baseTokenAccount"; + type: "publicKey"; + index: false; + }, + { + name: "quoteTokenAccount"; + type: "publicKey"; + index: false; + }, + { + name: "quoteMint"; + type: "publicKey"; + index: false; + }, + { + name: "baseMint"; + type: "publicKey"; + index: false; + }, + { + name: "quoteFeesCollected"; + type: "u64"; + index: false; + }, + { + name: "baseFeesCollected"; + type: "u64"; + index: false; + }, + ]; + }, ]; errors: [ { @@ -2991,6 +3121,11 @@ export type Futarchy = { name: "InvalidTargetK"; msg: "Target K must be greater than the current K"; }, + { + code: 6035; + name: "InvalidTransactionMessage"; + msg: "Failed to compile transaction message for Squads vault transaction"; + }, ]; }; @@ -3942,40 +4077,35 @@ export const IDL: Futarchy = { args: [], }, { - name: "collectLpFees", + name: "executeSpendingLimitChange", accounts: [ { - name: "dao", + name: "proposal", isMut: true, isSigner: false, }, { - name: "admin", - isMut: false, - isSigner: true, - }, - { - name: "baseTokenAccount", + name: "dao", isMut: true, isSigner: false, }, { - name: "quoteTokenAccount", + name: "squadsProposal", isMut: true, isSigner: false, }, { - name: "ammBaseVault", - isMut: true, + name: "squadsMultisig", + isMut: false, isSigner: false, }, { - name: "ammQuoteVault", - isMut: true, + name: "squadsMultisigProgram", + isMut: false, isSigner: false, }, { - name: "tokenProgram", + name: "vaultTransaction", isMut: false, isSigner: false, }, @@ -3990,17 +4120,10 @@ export const IDL: Futarchy = { isSigner: false, }, ], - args: [ - { - name: "args", - type: { - defined: "CollectLpFeesArgs", - }, - }, - ], + args: [], }, { - name: "executeSpendingLimitChange", + name: "sponsorProposal", accounts: [ { name: "proposal", @@ -4013,56 +4136,158 @@ export const IDL: Futarchy = { isSigner: false, }, { - name: "squadsProposal", - isMut: true, - isSigner: false, + name: "teamAddress", + isMut: false, + isSigner: true, }, { - name: "squadsMultisig", + name: "eventAuthority", isMut: false, isSigner: false, }, { - name: "squadsMultisigProgram", + name: "program", isMut: false, isSigner: false, }, + ], + args: [], + }, + { + name: "collectMeteoraDammFees", + accounts: [ { - name: "vaultTransaction", - isMut: false, + name: "dao", + isMut: true, isSigner: false, }, { - name: "eventAuthority", - isMut: false, + name: "admin", + isMut: true, + isSigner: true, + }, + { + name: "squadsMultisig", + isMut: true, isSigner: false, }, { - name: "program", + name: "squadsMultisigVault", isMut: false, isSigner: false, }, - ], - args: [], - }, - { - name: "sponsorProposal", - accounts: [ { - name: "proposal", + name: "squadsMultisigVaultTransaction", isMut: true, isSigner: false, }, { - name: "dao", + name: "squadsMultisigProposal", isMut: true, isSigner: false, }, { - name: "teamAddress", + name: "squadsMultisigPermissionlessAccount", isMut: false, isSigner: true, }, + { + name: "meteoraClaimPositionFeesAccounts", + accounts: [ + { + name: "dammV2Program", + isMut: false, + isSigner: false, + }, + { + name: "dammV2EventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "poolAuthority", + isMut: false, + isSigner: false, + }, + { + name: "pool", + isMut: false, + isSigner: false, + }, + { + name: "position", + isMut: true, + isSigner: false, + }, + { + name: "tokenAAccount", + isMut: true, + isSigner: false, + docs: ["Token account of base tokens recipient"], + }, + { + name: "tokenBAccount", + isMut: true, + isSigner: false, + docs: ["Token account of quote tokens recipient"], + }, + { + name: "tokenAVault", + isMut: true, + isSigner: false, + }, + { + name: "tokenBVault", + isMut: true, + isSigner: false, + }, + { + name: "tokenAMint", + isMut: false, + isSigner: false, + }, + { + name: "tokenBMint", + isMut: false, + isSigner: false, + }, + { + name: "positionNftAccount", + isMut: false, + isSigner: false, + }, + { + name: "owner", + isMut: false, + isSigner: false, + }, + { + name: "tokenAProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenBProgram", + isMut: false, + isSigner: false, + }, + ], + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "squadsProgram", + isMut: false, + isSigner: false, + }, { name: "eventAuthority", isMut: false, @@ -4396,18 +4621,6 @@ export const IDL: Futarchy = { ], }, }, - { - name: "CollectLpFeesArgs", - type: { - kind: "struct", - fields: [ - { - name: "targetK", - type: "u128", - }, - ], - }, - }, { name: "ConditionalSwapParams", type: { @@ -5810,6 +6023,58 @@ export const IDL: Futarchy = { }, ], }, + { + name: "CollectMeteoraDammFeesEvent", + fields: [ + { + name: "common", + type: { + defined: "CommonFields", + }, + index: false, + }, + { + name: "dao", + type: "publicKey", + index: false, + }, + { + name: "pool", + type: "publicKey", + index: false, + }, + { + name: "baseTokenAccount", + type: "publicKey", + index: false, + }, + { + name: "quoteTokenAccount", + type: "publicKey", + index: false, + }, + { + name: "quoteMint", + type: "publicKey", + index: false, + }, + { + name: "baseMint", + type: "publicKey", + index: false, + }, + { + name: "quoteFeesCollected", + type: "u64", + index: false, + }, + { + name: "baseFeesCollected", + type: "u64", + index: false, + }, + ], + }, ], errors: [ { @@ -5987,5 +6252,10 @@ export const IDL: Futarchy = { name: "InvalidTargetK", msg: "Target K must be greater than the current K", }, + { + code: 6035, + name: "InvalidTransactionMessage", + msg: "Failed to compile transaction message for Squads vault transaction", + }, ], }; diff --git a/sdk/src/v0.7/FutarchyClient.ts b/sdk/src/v0.7/FutarchyClient.ts index f2f7a492..4ad7e097 100644 --- a/sdk/src/v0.7/FutarchyClient.ts +++ b/sdk/src/v0.7/FutarchyClient.ts @@ -32,6 +32,11 @@ import { SQUADS_PROGRAM_ID, USDC_DECIMALS, SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + MAINNET_METEORA_CONFIG, + METADAO_MULTISIG_VAULT, + DAMM_V2_PROGRAM_ID, + LAUNCHPAD_PROGRAM_ID, + DAMM_V2_POOL_AUTHORITY, } from "./constants.js"; import { DEFAULT_CU_PRICE, @@ -987,4 +992,125 @@ export class FutarchyClient { teamAddress, }); } + + collectMeteoraDammFeesIx({ + dao, + baseMint, + quoteMint = MAINNET_USDC, + transactionIndex, + meteoraConfig = MAINNET_METEORA_CONFIG, + admin = this.provider.publicKey, + }: { + dao: PublicKey; + baseMint: PublicKey; + quoteMint?: PublicKey; + transactionIndex: bigint; + meteoraConfig?: PublicKey; + admin?: PublicKey; + }) { + // Squads accounts + const multisigPda = multisig.getMultisigPda({ createKey: dao })[0]; + const squadsMultisigVault = multisig.getVaultPda({ + multisigPda, + index: 0, + })[0]; + const squadsMultisigVaultTransaction = multisig.getTransactionPda({ + multisigPda, + index: transactionIndex, + })[0]; + const squadsMultisigProposal = multisig.getProposalPda({ + multisigPda, + transactionIndex, + })[0]; + + // Token accounts for receiving fees + const baseTokenAccount = getAssociatedTokenAddressSync( + baseMint, + METADAO_MULTISIG_VAULT, + true, + ); + const quoteTokenAccount = getAssociatedTokenAddressSync( + quoteMint, + METADAO_MULTISIG_VAULT, + true, + ); + + // Helper function to sort mints for Meteora pool PDA + const sortMints = ( + mint1: PublicKey, + mint2: PublicKey, + ): [Buffer, Buffer] => { + const buf1 = mint1.toBuffer(); + const buf2 = mint2.toBuffer(); + if (Buffer.compare(buf1, buf2) > 0) { + return [buf1, buf2]; + } + return [buf2, buf1]; + }; + + const [sortedMint1, sortedMint2] = sortMints(baseMint, quoteMint); + + // Meteora DAMM accounts + const [pool] = PublicKey.findProgramAddressSync( + [Buffer.from("pool"), meteoraConfig.toBuffer(), sortedMint1, sortedMint2], + DAMM_V2_PROGRAM_ID, + ); + + const [positionNftMint] = PublicKey.findProgramAddressSync( + [Buffer.from("position_nft_mint"), baseMint.toBuffer()], + LAUNCHPAD_PROGRAM_ID, + ); + + const [positionNftAccount] = PublicKey.findProgramAddressSync( + [Buffer.from("position_nft_account"), positionNftMint.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const [position] = PublicKey.findProgramAddressSync( + [Buffer.from("position"), positionNftMint.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const [tokenAVault] = PublicKey.findProgramAddressSync( + [Buffer.from("token_vault"), baseMint.toBuffer(), pool.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const [tokenBVault] = PublicKey.findProgramAddressSync( + [Buffer.from("token_vault"), quoteMint.toBuffer(), pool.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const [dammV2EventAuthority] = getEventAuthorityAddr(DAMM_V2_PROGRAM_ID); + + return this.autocrat.methods.collectMeteoraDammFees().accounts({ + dao, + admin, + squadsMultisig: multisigPda, + squadsMultisigVault, + squadsMultisigVaultTransaction, + squadsMultisigProposal, + squadsMultisigPermissionlessAccount: PERMISSIONLESS_ACCOUNT.publicKey, + meteoraClaimPositionFeesAccounts: { + dammV2Program: DAMM_V2_PROGRAM_ID, + dammV2EventAuthority, + poolAuthority: DAMM_V2_POOL_AUTHORITY, + pool, + position, + tokenAAccount: baseTokenAccount, + tokenBAccount: quoteTokenAccount, + tokenAVault, + tokenBVault, + tokenAMint: baseMint, + tokenBMint: quoteMint, + positionNftAccount, + owner: squadsMultisigVault, + tokenAProgram: TOKEN_PROGRAM_ID, + tokenBProgram: TOKEN_PROGRAM_ID, + }, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + squadsProgram: SQUADS_PROGRAM_ID, + }); + } } diff --git a/sdk/src/v0.7/LaunchpadClient.ts b/sdk/src/v0.7/LaunchpadClient.ts index 92c86f76..2a752b44 100644 --- a/sdk/src/v0.7/LaunchpadClient.ts +++ b/sdk/src/v0.7/LaunchpadClient.ts @@ -29,7 +29,7 @@ import { DAMM_V2_PROGRAM_ID, SQUADS_PROGRAM_CONFIG_TREASURY_DEVNET, MAINNET_METEORA_CONFIG, - FEE_RECIPIENT, + METADAO_MULTISIG_VAULT, } from "./constants.js"; import { ASSOCIATED_TOKEN_PROGRAM_ID, @@ -293,7 +293,7 @@ export class LaunchpadClient { launchAuthority, isDevnet = false, meteoraConfig = MAINNET_METEORA_CONFIG, - feeRecipient = FEE_RECIPIENT, + feeRecipient = METADAO_MULTISIG_VAULT, }: { launch: PublicKey; quoteMint?: PublicKey; diff --git a/sdk/src/v0.7/constants.ts b/sdk/src/v0.7/constants.ts index 98c72d7e..d3df833a 100644 --- a/sdk/src/v0.7/constants.ts +++ b/sdk/src/v0.7/constants.ts @@ -68,6 +68,10 @@ export const DAMM_V2_PROGRAM_ID = new PublicKey( "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG", ); +export const DAMM_V2_POOL_AUTHORITY = new PublicKey( + "HLnpSz9h2S4hiLQ43rnSD9XkcUThA7B8hQMKmDaiTLcC", +); + export const LOW_FEE_RAYDIUM_CONFIG = new PublicKey( "D4FPEruKEHrG5TenZ2mpDGEfu1iUvTiqBxvpU8HLBvC2", ); @@ -108,8 +112,7 @@ export const MAINNET_METEORA_CONFIG = new PublicKey( "FaA6RM9enPh1tU9Y8LiGCq715JubLc49WGcYTdNvDfsc", ); -// MetaDAO multisig vault -export const FEE_RECIPIENT = new PublicKey( +export const METADAO_MULTISIG_VAULT = new PublicKey( "6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf", ); diff --git a/sdk/src/v0.7/types/damm_v2_cpi.ts b/sdk/src/v0.7/types/damm_v2_cpi.ts index e9e83a82..44882927 100644 --- a/sdk/src/v0.7/types/damm_v2_cpi.ts +++ b/sdk/src/v0.7/types/damm_v2_cpi.ts @@ -121,6 +121,97 @@ export type DammV2Cpi = { }, ]; }, + { + name: "claimPositionFee"; + accounts: [ + { + name: "poolAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "pool"; + isMut: false; + isSigner: false; + }, + { + name: "position"; + isMut: true; + isSigner: false; + }, + { + name: "tokenAAccount"; + isMut: true; + isSigner: false; + docs: ["The user token a account"]; + }, + { + name: "tokenBAccount"; + isMut: true; + isSigner: false; + docs: ["The user token b account"]; + }, + { + name: "tokenAVault"; + isMut: true; + isSigner: false; + docs: ["The vault token account for input token"]; + }, + { + name: "tokenBVault"; + isMut: true; + isSigner: false; + docs: ["The vault token account for output token"]; + }, + { + name: "tokenAMint"; + isMut: false; + isSigner: false; + docs: ["The mint of token a"]; + }, + { + name: "tokenBMint"; + isMut: false; + isSigner: false; + docs: ["The mint of token b"]; + }, + { + name: "positionNftAccount"; + isMut: false; + isSigner: false; + docs: ["The token account for nft"]; + }, + { + name: "owner"; + isMut: false; + isSigner: true; + docs: ["owner of position"]; + }, + { + name: "tokenAProgram"; + isMut: false; + isSigner: false; + docs: ["Token a program"]; + }, + { + name: "tokenBProgram"; + isMut: false; + isSigner: false; + docs: ["Token b program"]; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + }, + ]; + args: []; + }, ]; types: [ { @@ -404,6 +495,97 @@ export const IDL: DammV2Cpi = { }, ], }, + { + name: "claimPositionFee", + accounts: [ + { + name: "poolAuthority", + isMut: false, + isSigner: false, + }, + { + name: "pool", + isMut: false, + isSigner: false, + }, + { + name: "position", + isMut: true, + isSigner: false, + }, + { + name: "tokenAAccount", + isMut: true, + isSigner: false, + docs: ["The user token a account"], + }, + { + name: "tokenBAccount", + isMut: true, + isSigner: false, + docs: ["The user token b account"], + }, + { + name: "tokenAVault", + isMut: true, + isSigner: false, + docs: ["The vault token account for input token"], + }, + { + name: "tokenBVault", + isMut: true, + isSigner: false, + docs: ["The vault token account for output token"], + }, + { + name: "tokenAMint", + isMut: false, + isSigner: false, + docs: ["The mint of token a"], + }, + { + name: "tokenBMint", + isMut: false, + isSigner: false, + docs: ["The mint of token b"], + }, + { + name: "positionNftAccount", + isMut: false, + isSigner: false, + docs: ["The token account for nft"], + }, + { + name: "owner", + isMut: false, + isSigner: true, + docs: ["owner of position"], + }, + { + name: "tokenAProgram", + isMut: false, + isSigner: false, + docs: ["Token a program"], + }, + { + name: "tokenBProgram", + isMut: false, + isSigner: false, + docs: ["Token b program"], + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, ], types: [ { diff --git a/sdk/src/v0.7/types/futarchy.ts b/sdk/src/v0.7/types/futarchy.ts index 62f92aaa..90068312 100644 --- a/sdk/src/v0.7/types/futarchy.ts +++ b/sdk/src/v0.7/types/futarchy.ts @@ -1022,6 +1022,154 @@ export type Futarchy = { ]; args: []; }, + { + name: "collectMeteoraDammFees"; + accounts: [ + { + name: "dao"; + isMut: true; + isSigner: false; + }, + { + name: "admin"; + isMut: true; + isSigner: true; + }, + { + name: "squadsMultisig"; + isMut: true; + isSigner: false; + }, + { + name: "squadsMultisigVault"; + isMut: false; + isSigner: false; + }, + { + name: "squadsMultisigVaultTransaction"; + isMut: true; + isSigner: false; + }, + { + name: "squadsMultisigProposal"; + isMut: true; + isSigner: false; + }, + { + name: "squadsMultisigPermissionlessAccount"; + isMut: false; + isSigner: true; + }, + { + name: "meteoraClaimPositionFeesAccounts"; + accounts: [ + { + name: "dammV2Program"; + isMut: false; + isSigner: false; + }, + { + name: "dammV2EventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "poolAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "pool"; + isMut: false; + isSigner: false; + }, + { + name: "position"; + isMut: true; + isSigner: false; + }, + { + name: "tokenAAccount"; + isMut: true; + isSigner: false; + docs: ["Token account of base tokens recipient"]; + }, + { + name: "tokenBAccount"; + isMut: true; + isSigner: false; + docs: ["Token account of quote tokens recipient"]; + }, + { + name: "tokenAVault"; + isMut: true; + isSigner: false; + }, + { + name: "tokenBVault"; + isMut: true; + isSigner: false; + }, + { + name: "tokenAMint"; + isMut: false; + isSigner: false; + }, + { + name: "tokenBMint"; + isMut: false; + isSigner: false; + }, + { + name: "positionNftAccount"; + isMut: false; + isSigner: false; + }, + { + name: "owner"; + isMut: false; + isSigner: false; + }, + { + name: "tokenAProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenBProgram"; + isMut: false; + isSigner: false; + }, + ]; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "squadsProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + }, + ]; + args: []; + }, { name: "resizeDao"; accounts: [ @@ -2744,6 +2892,58 @@ export type Futarchy = { }, ]; }, + { + name: "CollectMeteoraDammFeesEvent"; + fields: [ + { + name: "common"; + type: { + defined: "CommonFields"; + }; + index: false; + }, + { + name: "dao"; + type: "publicKey"; + index: false; + }, + { + name: "pool"; + type: "publicKey"; + index: false; + }, + { + name: "baseTokenAccount"; + type: "publicKey"; + index: false; + }, + { + name: "quoteTokenAccount"; + type: "publicKey"; + index: false; + }, + { + name: "quoteMint"; + type: "publicKey"; + index: false; + }, + { + name: "baseMint"; + type: "publicKey"; + index: false; + }, + { + name: "quoteFeesCollected"; + type: "u64"; + index: false; + }, + { + name: "baseFeesCollected"; + type: "u64"; + index: false; + }, + ]; + }, ]; errors: [ { @@ -2921,6 +3121,11 @@ export type Futarchy = { name: "InvalidTargetK"; msg: "Target K must be greater than the current K"; }, + { + code: 6035; + name: "InvalidTransactionMessage"; + msg: "Failed to compile transaction message for Squads vault transaction"; + }, ]; }; @@ -3948,6 +4153,154 @@ export const IDL: Futarchy = { ], args: [], }, + { + name: "collectMeteoraDammFees", + accounts: [ + { + name: "dao", + isMut: true, + isSigner: false, + }, + { + name: "admin", + isMut: true, + isSigner: true, + }, + { + name: "squadsMultisig", + isMut: true, + isSigner: false, + }, + { + name: "squadsMultisigVault", + isMut: false, + isSigner: false, + }, + { + name: "squadsMultisigVaultTransaction", + isMut: true, + isSigner: false, + }, + { + name: "squadsMultisigProposal", + isMut: true, + isSigner: false, + }, + { + name: "squadsMultisigPermissionlessAccount", + isMut: false, + isSigner: true, + }, + { + name: "meteoraClaimPositionFeesAccounts", + accounts: [ + { + name: "dammV2Program", + isMut: false, + isSigner: false, + }, + { + name: "dammV2EventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "poolAuthority", + isMut: false, + isSigner: false, + }, + { + name: "pool", + isMut: false, + isSigner: false, + }, + { + name: "position", + isMut: true, + isSigner: false, + }, + { + name: "tokenAAccount", + isMut: true, + isSigner: false, + docs: ["Token account of base tokens recipient"], + }, + { + name: "tokenBAccount", + isMut: true, + isSigner: false, + docs: ["Token account of quote tokens recipient"], + }, + { + name: "tokenAVault", + isMut: true, + isSigner: false, + }, + { + name: "tokenBVault", + isMut: true, + isSigner: false, + }, + { + name: "tokenAMint", + isMut: false, + isSigner: false, + }, + { + name: "tokenBMint", + isMut: false, + isSigner: false, + }, + { + name: "positionNftAccount", + isMut: false, + isSigner: false, + }, + { + name: "owner", + isMut: false, + isSigner: false, + }, + { + name: "tokenAProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenBProgram", + isMut: false, + isSigner: false, + }, + ], + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "squadsProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, { name: "resizeDao", accounts: [ @@ -5670,6 +6023,58 @@ export const IDL: Futarchy = { }, ], }, + { + name: "CollectMeteoraDammFeesEvent", + fields: [ + { + name: "common", + type: { + defined: "CommonFields", + }, + index: false, + }, + { + name: "dao", + type: "publicKey", + index: false, + }, + { + name: "pool", + type: "publicKey", + index: false, + }, + { + name: "baseTokenAccount", + type: "publicKey", + index: false, + }, + { + name: "quoteTokenAccount", + type: "publicKey", + index: false, + }, + { + name: "quoteMint", + type: "publicKey", + index: false, + }, + { + name: "baseMint", + type: "publicKey", + index: false, + }, + { + name: "quoteFeesCollected", + type: "u64", + index: false, + }, + { + name: "baseFeesCollected", + type: "u64", + index: false, + }, + ], + }, ], errors: [ { @@ -5847,5 +6252,10 @@ export const IDL: Futarchy = { name: "InvalidTargetK", msg: "Target K must be greater than the current K", }, + { + code: 6035, + name: "InvalidTransactionMessage", + msg: "Failed to compile transaction message for Squads vault transaction", + }, ], }; diff --git a/tests/futarchy/main.test.ts b/tests/futarchy/main.test.ts index 64449f11..37478c3a 100644 --- a/tests/futarchy/main.test.ts +++ b/tests/futarchy/main.test.ts @@ -9,7 +9,37 @@ import conditionalSwap from "./unit/conditionalSwap.test.js"; import executeSpendingLimitChange from "./unit/executeSpendingLimitChange.test.js"; +import collectMeteoraDammFees from "./unit/collectMeteoraDammFees.test.js"; +import { PublicKey } from "@solana/web3.js"; +import { + LAUNCHPAD_PROGRAM_ID, + MAINNET_METEORA_CONFIG, +} from "@metadaoproject/futarchy/v0.7"; + export default function suite() { + before(async function () { + const dynamicConfig = await this.banksClient.getAccount( + new PublicKey("4mPQ4VuvvtYL3CeMPt14Uj1CLpBWcVdJoLoTH9ea4Kod"), + ); + + // discriminator + vault config authority + const poolCreatorAuthorityOffset = 8 + 32; + // discriminator + vault config authority + pool creator authority + pool fees config + activation type + collect fee mode + const configTypeOffset = 8 + 32 + 32 + 128 + 1 + 1; + + const [poolCreatorAuthority] = PublicKey.findProgramAddressSync( + [Buffer.from("damm_pool_creator_authority")], + LAUNCHPAD_PROGRAM_ID, + ); + + dynamicConfig.data.set( + poolCreatorAuthority.toBuffer(), + poolCreatorAuthorityOffset, + ); + dynamicConfig.data.set([1], configTypeOffset); + + this.context.setAccount(MAINNET_METEORA_CONFIG, dynamicConfig); + }); describe("#initialize_dao", initializeDao); describe("#initialize_proposal", initializeProposal); describe("#finalize_proposal", finalizeProposal); @@ -17,6 +47,9 @@ export default function suite() { describe("#collect_fees", collectFees); describe("#conditional_swap", conditionalSwap); describe("#execute_spending_limit_change", executeSpendingLimitChange); + + describe("#collect_meteora_damm_fees", collectMeteoraDammFees); + // describe("full proposal", fullProposal); // describe("proposal with a squads batch tx", proposalBatchTx); describe("futarchy amm", futarchyAmm); diff --git a/tests/futarchy/unit/collectMeteoraDammFees.test.ts b/tests/futarchy/unit/collectMeteoraDammFees.test.ts new file mode 100644 index 00000000..5ecef450 --- /dev/null +++ b/tests/futarchy/unit/collectMeteoraDammFees.test.ts @@ -0,0 +1,339 @@ +import { + ComputeBudgetProgram, + Keypair, + PublicKey, + Transaction, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { + DAMM_V2_PROGRAM_ID, + FutarchyClient, + LaunchpadClient, + MAINNET_METEORA_CONFIG, + MAINNET_USDC, + PERMISSIONLESS_ACCOUNT, +} from "@metadaoproject/futarchy/v0.7"; +import { BN } from "bn.js"; +import { initializeMintWithSeeds } from "../../launchpad_v7/utils.js"; +import { createLookupTableForTransaction } from "../../utils.js"; +import * as multisig from "@sqds/multisig"; +import { assert } from "chai"; +import { + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { METADAO_MULTISIG_VAULT } from "../../../sdk/src/v0.7/constants.js"; +import { CpAmm } from "@meteora-ag/cp-amm-sdk"; + +export default function suite() { + let futarchyClient: FutarchyClient; + let launchpadClient: LaunchpadClient; + let METAKP: Keypair; + let META: PublicKey; + let launch: PublicKey; + let launchSigner: PublicKey; + + const minRaise = new BN(1000_000000); // 1000 USDC + const secondsForLaunch = 60 * 60 * 24 * 7; // 1 week + const monthlySpend = new BN(100_000000); + const recipientAddress = Keypair.generate().publicKey; + const premineAmount = new BN(500_000_000); + + before(async function () { + futarchyClient = this.futarchy; + launchpadClient = this.launchpad_v7; + }); + + beforeEach(async function () { + const result = await initializeMintWithSeeds( + this.banksClient, + this.launchpad_v7, + this.payer, + ); + + META = result.tokenMint; + launch = result.launch; + launchSigner = result.launchSigner; + + // Initialize launch + await launchpadClient + .initializeLaunchIx({ + tokenName: "META", + tokenSymbol: "META", + tokenUri: "https://example.com", + minimumRaiseAmount: minRaise, + secondsForLaunch: secondsForLaunch, + baseMint: META, + quoteMint: MAINNET_USDC, + monthlySpendingLimitAmount: monthlySpend, // 100 USDC burn + monthlySpendingLimitMembers: [this.payer.publicKey], + performancePackageGrantee: recipientAddress, + performancePackageTokenAmount: premineAmount, + monthsUntilInsidersCanUnlock: 18, + teamAddress: PublicKey.default, + }) + .rpc(); + + await launchpadClient.startLaunchIx({ launch }).rpc(); + await this.createTokenAccount(META, this.payer.publicKey); + + await launchpadClient.fundIx({ launch, amount: minRaise }).rpc(); + + // Advance clock past 7 days + await this.advanceBySeconds(60 * 60 * 24 * 11); + + await launchpadClient.closeLaunchIx({ launch }).rpc(); + + await launchpadClient + .setFundingRecordApprovalIx({ + launch, + funder: this.payer.publicKey, + approvedAmount: minRaise, + launchAuthority: this.payer.publicKey, + }) + .rpc(); + + const completeLaunchTx = await launchpadClient + .completeLaunchIx({ + launch, + quoteMint: MAINNET_USDC, + baseMint: META, + launchAuthority: this.payer.publicKey, + }) + .transaction(); + + const completeLaunchLut = await createLookupTableForTransaction( + completeLaunchTx, + this, + ); + + const completeLaunchMessage = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: completeLaunchTx.instructions, + }).compileToV0Message([completeLaunchLut]); + + const tx = new VersionedTransaction(completeLaunchMessage); + tx.sign([this.payer]); + + await this.banksClient.processTransaction(tx); + }); + + it("collects Meteora DAMM fees successfully after a successful launch", async function () { + const payerUsdcTokenAccount = getAssociatedTokenAddressSync( + MAINNET_USDC, + this.payer.publicKey, + true, + ); + const payerMetaTokenAccount = getAssociatedTokenAddressSync( + META, + this.payer.publicKey, + true, + ); + + let payerUsdcTokenBalanceBeforeSwap = await this.getTokenBalance( + MAINNET_USDC, + this.payer.publicKey, + ); + let payerMetaTokenBalanceBeforeSwap = await this.getTokenBalance( + META, + this.payer.publicKey, + ); + + // Assert that the payer has no META tokens before the swap + assert.equal(payerMetaTokenBalanceBeforeSwap, 0n); + + /////////////////////////// + // Swap to generate fees // + /////////////////////////// + + // Helper function to sort mints for Meteora pool PDA + const sortMints = ( + mint1: PublicKey, + mint2: PublicKey, + ): [Buffer, Buffer] => { + const buf1 = mint1.toBuffer(); + const buf2 = mint2.toBuffer(); + if (Buffer.compare(buf1, buf2) > 0) { + return [buf1, buf2]; + } + return [buf2, buf1]; + }; + + const [sortedMint1, sortedMint2] = sortMints(MAINNET_USDC, META); + + const cpAmm = new CpAmm(this.squadsConnection); + + const [pool] = PublicKey.findProgramAddressSync( + [ + Buffer.from("pool"), + MAINNET_METEORA_CONFIG.toBuffer(), + sortedMint1, + sortedMint2, + ], + DAMM_V2_PROGRAM_ID, + ); + + const [tokenAVault] = PublicKey.findProgramAddressSync( + [Buffer.from("token_vault"), META.toBuffer(), pool.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const [tokenBVault] = PublicKey.findProgramAddressSync( + [Buffer.from("token_vault"), MAINNET_USDC.toBuffer(), pool.toBuffer()], + DAMM_V2_PROGRAM_ID, + ); + + const swapTx = await cpAmm._program.methods + .swap({ + amountIn: new BN(1_000_000), // 1 USDC swap + minimumAmountOut: new BN(0), + }) + .accounts({ + tokenAMint: META, + tokenBMint: MAINNET_USDC, + tokenAProgram: TOKEN_PROGRAM_ID, + tokenBProgram: TOKEN_PROGRAM_ID, + referralTokenAccount: null, + inputTokenAccount: payerUsdcTokenAccount, + outputTokenAccount: payerMetaTokenAccount, + payer: this.payer.publicKey, + pool: pool, + program: DAMM_V2_PROGRAM_ID, + tokenAVault: tokenAVault, + tokenBVault: tokenBVault, + }) + .transaction(); + + swapTx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + swapTx.feePayer = this.payer.publicKey; + swapTx.sign(this.payer); + + await this.banksClient.processTransaction(swapTx); + + let payerUsdcTokenBalanceAfterSwap = await this.getTokenBalance( + MAINNET_USDC, + this.payer.publicKey, + ); + let payerMetaTokenBalanceAfterSwap = await this.getTokenBalance( + META, + this.payer.publicKey, + ); + + // Payer spent 1 USDC for swap + assert.equal( + payerUsdcTokenBalanceAfterSwap, + payerUsdcTokenBalanceBeforeSwap - 1_000_000n, + ); + // Payer received META tokens from swap + assert(payerMetaTokenBalanceAfterSwap > 0n); + + ////////////////// + // Extract fees // + ////////////////// + + const launchAccount = await launchpadClient.fetchLaunch(launch); + + let daoAccount = await futarchyClient.getDao(launchAccount.dao); + + const squadsMultisigAccount = + await multisig.accounts.Multisig.fromAccountAddress( + this.squadsConnection, + daoAccount.squadsMultisig, + ); + + const metaDaoBaseTokenAccount = getAssociatedTokenAddressSync( + META, + METADAO_MULTISIG_VAULT, + true, + ); + + const metaDaoQuoteTokenAccount = getAssociatedTokenAddressSync( + MAINNET_USDC, + METADAO_MULTISIG_VAULT, + true, + ); + + const metaDaoBaseTokenBalanceBeforeCollect = await this.getTokenBalance( + META, + METADAO_MULTISIG_VAULT, + ); + const metaDaoQuoteTokenBalanceBeforeCollect = await this.getTokenBalance( + MAINNET_USDC, + METADAO_MULTISIG_VAULT, + ); + + assert.equal(metaDaoBaseTokenBalanceBeforeCollect, 0n); + assert.equal(metaDaoQuoteTokenBalanceBeforeCollect, 0n); + + const tx = new Transaction() + .add( + createAssociatedTokenAccountIdempotentInstruction( + this.payer.publicKey, + metaDaoBaseTokenAccount, + METADAO_MULTISIG_VAULT, + META, + ), + ) + .add( + createAssociatedTokenAccountIdempotentInstruction( + this.payer.publicKey, + metaDaoQuoteTokenAccount, + METADAO_MULTISIG_VAULT, + MAINNET_USDC, + ), + ); + + tx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + tx.feePayer = this.payer.publicKey; + tx.sign(this.payer); + + await this.banksClient.processTransaction(tx); + + daoAccount = await futarchyClient.getDao(launchAccount.dao); + + const daoSeqNumBeforeCollect = daoAccount.seqNum; + + await futarchyClient + .collectMeteoraDammFeesIx({ + dao: launchAccount.dao, + baseMint: META, + quoteMint: MAINNET_USDC, + transactionIndex: + BigInt(squadsMultisigAccount.transactionIndex.toString()) + 1n, + meteoraConfig: MAINNET_METEORA_CONFIG, + admin: this.payer.publicKey, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_200_000 }), + ]) + .signers([this.payer, PERMISSIONLESS_ACCOUNT]) + .rpc(); + + daoAccount = await futarchyClient.getDao(launchAccount.dao); + + const daoSeqNumAfterCollect = daoAccount.seqNum; + + // Assert that the DAO sequence number was incremented + assert.equal( + daoSeqNumAfterCollect.toString(), + daoSeqNumBeforeCollect.add(new BN(1)).toString(), + ); + + const metaDaoBaseTokenBalanceAfterCollect = await this.getTokenBalance( + META, + METADAO_MULTISIG_VAULT, + ); + const metaDaoQuoteTokenBalanceAfterCollect = await this.getTokenBalance( + MAINNET_USDC, + METADAO_MULTISIG_VAULT, + ); + + // MetaDAO received base tokens from swap + assert(metaDaoBaseTokenBalanceAfterCollect > 0n); + // MetaDAO received no quote tokens from swap, as the swap was not in that direction + assert.equal(metaDaoQuoteTokenBalanceAfterCollect, 0n); + }); +} diff --git a/yarn.lock b/yarn.lock index 2d534b81..702b1b9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37,6 +37,11 @@ resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz#bdfd3a353131345244546876eb4afc0e125bec30" integrity sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ== +"@coral-xyz/anchor-errors@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.31.1.tgz#d635cbac2533973ae6bfb5d3ba1de89ce5aece2d" + integrity sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ== + "@coral-xyz/anchor@=0.29.0", "@coral-xyz/anchor@^0.29.0": version "0.29.0" resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.29.0.tgz#bd0be95bedfb30a381c3e676e5926124c310ff12" @@ -78,6 +83,25 @@ superstruct "^0.15.4" toml "^3.0.0" +"@coral-xyz/anchor@^0.31.0": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.31.1.tgz#0fdeebf45a3cb2e47e8ebbb815ca98542152962c" + integrity sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA== + dependencies: + "@coral-xyz/anchor-errors" "^0.31.1" + "@coral-xyz/borsh" "^0.31.1" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.69.0" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + eventemitter3 "^4.0.7" + pako "^2.0.3" + superstruct "^0.15.4" + toml "^3.0.0" + "@coral-xyz/borsh@^0.29.0": version "0.29.0" resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.29.0.tgz#79f7045df2ef66da8006d47f5399c7190363e71f" @@ -94,6 +118,14 @@ bn.js "^5.1.2" buffer-layout "^1.2.0" +"@coral-xyz/borsh@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.31.1.tgz#5328e1e0921b75d7f4a62dd3f61885a938bc7241" + integrity sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -1148,6 +1180,19 @@ "@metaplex-foundation/umi-public-keys" "^0.8.9" "@metaplex-foundation/umi-serializers" "^0.9.0" +"@meteora-ag/cp-amm-sdk@^1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@meteora-ag/cp-amm-sdk/-/cp-amm-sdk-1.2.6.tgz#9eb24abc0ed3b09eb96aabfe5af5e8d56637ebe5" + integrity sha512-EYNk6/6/AwMAyFCwA9ZG1svJfUOM8cn8mpg/un/PkImuNSrbRlN9Apiw+mRiwLvrEiCjSVUCDyGJofr/eX5TFQ== + dependencies: + "@coral-xyz/anchor" "^0.31.0" + "@solana/spl-token" "^0.4.8" + "@solana/web3.js" "^1.95.3" + "@types/bn.js" "^5.1.0" + chain "^0.4.0" + decimal.js "^10.4.2" + invariant "^2.2.4" + "@noble/curves@^1.0.0", "@noble/curves@^1.4.2": version "1.9.7" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" @@ -1318,7 +1363,14 @@ dependencies: buffer "^6.0.3" -"@solana/spl-token-metadata@^0.1.2": +"@solana/spl-token-group@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz#83c00f0cd0bda33115468cd28b89d94f8ec1fee4" + integrity sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token-metadata@^0.1.2", "@solana/spl-token-metadata@^0.1.6": version "0.1.6" resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz#d240947aed6e7318d637238022a7b0981b32ae80" integrity sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA== @@ -1342,6 +1394,17 @@ "@solana/spl-token-metadata" "^0.1.2" buffer "^6.0.3" +"@solana/spl-token@^0.4.8": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.14.tgz#b86bc8a17f50e9680137b585eca5f5eb9d55c025" + integrity sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-group" "^0.0.7" + "@solana/spl-token-metadata" "^0.1.6" + buffer "^6.0.3" + "@solana/wallet-adapter-base@^0.9.2": version "0.9.27" resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.27.tgz#f76463db172ac1d7d1f5aa064800363777731dfd" @@ -1381,7 +1444,7 @@ rpc-websockets "^7.5.1" superstruct "^0.14.2" -"@solana/web3.js@>1.92.0, <2.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.70.3", "@solana/web3.js@^1.76.0", "@solana/web3.js@^1.98.4": +"@solana/web3.js@>1.92.0, <2.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.69.0", "@solana/web3.js@^1.70.3", "@solana/web3.js@^1.76.0", "@solana/web3.js@^1.95.3", "@solana/web3.js@^1.98.4": version "1.98.4" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.4.tgz#df51d78be9d865181ec5138b4e699d48e6895bbe" integrity sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw== @@ -2179,6 +2242,11 @@ chai@^4.3.4: pathval "^1.1.1" type-detect "^4.1.0" +chain@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/chain/-/chain-0.4.2.tgz#f79588b10f9c3a795f012d4f5fd4f3cc01a9ea4e" + integrity sha512-GtM+TlN398yBhtSp1D2dBLQomKM3Umbji3h2/NdCqAWSMKhWbjlz33j0e55rStsEZD+8OLRHuz7kWd0U3xKMDg== + chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -2426,7 +2494,7 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -decimal.js@^10.4.3: +decimal.js@^10.4.2, decimal.js@^10.4.3: version "10.6.0" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== @@ -3197,7 +3265,7 @@ inquirer@^8.2.0: through "^2.3.6" wrap-ansi "^6.0.1" -invariant@2.2.4: +invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==