diff --git a/programs/wewe-token-launch-pad/src/instructions/admin/ix_collect_pool_fees.rs b/programs/wewe-token-launch-pad/src/instructions/admin/ix_collect_pool_fees.rs index bba5c64..9ea0a06 100644 --- a/programs/wewe-token-launch-pad/src/instructions/admin/ix_collect_pool_fees.rs +++ b/programs/wewe-token-launch-pad/src/instructions/admin/ix_collect_pool_fees.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, - token::Token, + token::{close_account, CloseAccount, Token}, token_interface::{TokenAccount, TokenInterface}, }; @@ -52,6 +52,7 @@ pub struct ClaimPositionFee<'info> { /// CHECK: pub token_b_mint: UncheckedAccount<'info>, + /// WSOL account - can be owned by treasury or vault_authority (we'll use as temp account) #[account( init_if_needed, payer = payer, @@ -68,6 +69,7 @@ pub struct ClaimPositionFee<'info> { )] pub wewe_token_account: Box>, + /// WSOL account - can be owned by maker or vault_authority (we'll use as temp account) #[account( init_if_needed, payer = payer, @@ -118,6 +120,44 @@ pub struct ClaimPositionFee<'info> { /// CHECK: pub position_nft_account: UncheckedAccount<'info>, + /// CHECK: Temporary WSOL account for treasury unwrapping (PDA derived from token program) + /// PDA: [b"temp_wsol", vault_authority, proposal, b"treasury"] + #[account( + mut, + constraint = { + let (expected_pda, _) = Pubkey::find_program_address( + &[ + b"temp_wsol", + vault_authority.key().as_ref(), + proposal.key().as_ref(), + b"treasury", + ], + &token_b_program.key(), + ); + treasury_temp_wsol.key() == expected_pda + } @ ProposalError::IncorrectAccount + )] + pub treasury_temp_wsol: UncheckedAccount<'info>, + + /// CHECK: Temporary WSOL account for maker unwrapping (PDA derived from token program) + /// PDA: [b"temp_wsol", vault_authority, proposal, b"maker"] + #[account( + mut, + constraint = { + let (expected_pda, _) = Pubkey::find_program_address( + &[ + b"temp_wsol", + vault_authority.key().as_ref(), + proposal.key().as_ref(), + b"maker", + ], + &token_b_program.key(), + ); + maker_temp_wsol.key() == expected_pda + } @ ProposalError::IncorrectAccount + )] + pub maker_temp_wsol: UncheckedAccount<'info>, + pub token_a_program: Interface<'info, TokenInterface>, pub token_b_program: Interface<'info, TokenInterface>, @@ -217,34 +257,91 @@ impl<'info> ClaimPositionFee<'info> { )?; } + // Unwrap WSOL (token_b) to SOL before transferring + // We use PDA-derived temporary accounts (validated at account struct level) if treasury_b > 0 { + // Transfer WSOL to temporary PDA account anchor_spl::token::transfer( CpiContext::new_with_signer( self.token_b_program.to_account_info(), anchor_spl::token::Transfer { from: self.token_b_account.to_account_info(), - to: self.wewe_wsol_account.to_account_info(), + to: self.treasury_temp_wsol.to_account_info(), authority: self.vault_authority.to_account_info(), }, &[&vault_authority_seeds[..]], ), treasury_b, )?; + + // Close the temporary WSOL account to unwrap it to SOL + // The SOL (lamports) will be sent to the account owner (vault_authority) + close_account( + CpiContext::new_with_signer( + self.token_b_program.to_account_info(), + CloseAccount { + account: self.treasury_temp_wsol.to_account_info(), + destination: self.vault_authority.to_account_info(), + authority: self.vault_authority.to_account_info(), + }, + &[&vault_authority_seeds[..]], + ), + )?; + + // Transfer SOL from vault_authority to treasury + anchor_lang::system_program::transfer( + CpiContext::new_with_signer( + self.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: self.vault_authority.to_account_info(), + to: self.wewe_treasury.to_account_info(), + }, + &[&vault_authority_seeds[..]], + ), + treasury_b, + )?; } if maker_b > 0 { + // Transfer WSOL to temporary PDA account anchor_spl::token::transfer( CpiContext::new_with_signer( self.token_b_program.to_account_info(), anchor_spl::token::Transfer { from: self.token_b_account.to_account_info(), - to: self.maker_wsol_account.to_account_info(), + to: self.maker_temp_wsol.to_account_info(), authority: self.vault_authority.to_account_info(), }, &[&vault_authority_seeds[..]], ), maker_b, )?; + + // Close the temporary WSOL account to unwrap it to SOL + close_account( + CpiContext::new_with_signer( + self.token_b_program.to_account_info(), + CloseAccount { + account: self.maker_temp_wsol.to_account_info(), + destination: self.vault_authority.to_account_info(), + authority: self.vault_authority.to_account_info(), + }, + &[&vault_authority_seeds[..]], + ), + )?; + + // Transfer SOL from vault_authority to maker + anchor_lang::system_program::transfer( + CpiContext::new_with_signer( + self.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: self.vault_authority.to_account_info(), + to: self.maker.to_account_info(), + }, + &[&vault_authority_seeds[..]], + ), + maker_b, + )?; } emit!(PositionFeeClaimed { diff --git a/tests/utils.ts b/tests/utils.ts index a92f8d6..8730f7b 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,7 +1,7 @@ // utils/helpers.ts import * as anchor from '@coral-xyz/anchor'; import Decimal from "decimal.js"; -import { getAssociatedTokenAddressSync } from '@solana/spl-token'; +import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from '@solana/spl-token'; import BN from "bn.js"; export const WSOL_MINT = new anchor.web3.PublicKey("So11111111111111111111111111111111111111112"); @@ -119,6 +119,24 @@ export function findMetadataPDA(mint: anchor.web3.PublicKey): anchor.web3.Public return metadataPDA; } +export function findTempWsolPDA( + vaultAuthority: anchor.web3.PublicKey, + proposal: anchor.web3.PublicKey, + isTreasury: boolean +): anchor.web3.PublicKey { + const [tempWsolPDA] = anchor.web3.PublicKey.findProgramAddressSync( + [ + Buffer.from("temp_wsol"), + vaultAuthority.toBuffer(), + proposal.toBuffer(), + Buffer.from(isTreasury ? "treasury" : "maker"), + ], + TOKEN_PROGRAM_ID + ); + + return tempWsolPDA; +} + export const derivePoolPDAs = ( programId: anchor.web3.PublicKey, cpAmmProgramId: anchor.web3.PublicKey, diff --git a/tests/wewe-token-launch-pad.ts b/tests/wewe-token-launch-pad.ts index 35ae36e..abaa6b9 100644 --- a/tests/wewe-token-launch-pad.ts +++ b/tests/wewe-token-launch-pad.ts @@ -37,6 +37,7 @@ import { findConfigPDA, findBackerProposalCountPDA, findMetadataPDA, + findTempWsolPDA, } from './utils'; const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); @@ -1494,6 +1495,8 @@ describe('Wewe Token Launch Pad - Integration Tests', () => { const weweTokenAccount = getAssociatedTokenAddressSync(mint.publicKey, weweTreasury, true); const makerWsolAccount = getAssociatedTokenAddressSync(WSOL_MINT, maker.publicKey, true); const [wsolVault] = getTokenVaultAddress(vaultAuthority, WSOL_MINT, program.programId); + const treasuryTempWsol = findTempWsolPDA(vaultAuthority, proposal, true); + const makerTempWsol = findTempWsolPDA(vaultAuthority, proposal, false); const computeUnitsIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }); const tx = await program.methods @@ -1518,6 +1521,8 @@ describe('Wewe Token Launch Pad - Integration Tests', () => { tokenAMint: mint.publicKey, tokenBMint: WSOL_MINT, positionNftAccount: pdas.positionNftAccount, + treasuryTempWsol, + makerTempWsol, tokenAProgram: TOKEN_PROGRAM_ID, tokenBProgram: TOKEN_PROGRAM_ID, ammProgram: cpAmm.programId, @@ -2481,6 +2486,8 @@ describe('Wewe Token Launch Pad - Integration Tests', () => { const weweTokenAccount = getAssociatedTokenAddressSync(mint.publicKey, weweTreasury, true); const makerWsolAccount = getAssociatedTokenAddressSync(WSOL_MINT, maker.publicKey, true); const [wsolVault] = getTokenVaultAddress(vaultAuthority, WSOL_MINT, program.programId); + const treasuryTempWsol = findTempWsolPDA(vaultAuthority, proposal, true); + const makerTempWsol = findTempWsolPDA(vaultAuthority, proposal, false); try { await program.methods @@ -2505,6 +2512,8 @@ describe('Wewe Token Launch Pad - Integration Tests', () => { tokenAMint: mint.publicKey, tokenBMint: WSOL_MINT, positionNftAccount: pdas.positionNftAccount, + treasuryTempWsol, + makerTempWsol, tokenAProgram: TOKEN_PROGRAM_ID, tokenBProgram: TOKEN_PROGRAM_ID, ammProgram: cpAmm.programId,