2

I have a program with 6 instructions, they all pass and this is the last one before I move client-side and server-side. In the prior instruction, I successfully use invoke() to transfer SOL from the payers account to a PDA pot that is a shared account that any Signer can write to (it's for a game). Now since for that previous instruction, it was going from the payer's account to the pot, I could just use invoke(). But to transfer to a winner requires the program to sign the transaction using invoke_signed() because when that account is initialized as a PDA, the Signer's publicKey isn't passed into the seed. But when I run the test, I get Transaction simulation failed: Error processing Instruction 0: Program failed to complete.

I think the issue is somewhere within the invoked_signed() and the way the seeds are being passed in.

claim_prize.rs

use anchor_lang::prelude::*;
use solana_program::native_token::LAMPORTS_PER_SOL;
use crate::errors::GameErrors;
use crate::state::state::{GameData, GamePot, Player, WordVault};
use anchor_lang::{
    prelude::*,
    solana_program::{ program::invoke_signed, system_instruction::transfer },
};

#[derive(Accounts)]
#[instruction(secret_word: String)]
pub struct ClaimVictory<'info> {
    #[account(mut)]
    pub game: Account<'info, GameData>,
    #[account(mut)]
    pub word_vault: Account<'info, WordVault>,
    #[account(mut)]
    pub player: Account<'info, Player>,
    #[account(mut)]
    pub money_pot_vault: Account<'info, GamePot>,
    #[account(mut)]
    pub owner: Signer<'info>,
    pub system_program: Program<'info, System>,
}

pub fn handle_claim_victory(ctx: Context<ClaimVictory>, secret_word: String, bump: u8) -> Result<()> {
    let word = &mut ctx.accounts.word_vault;
    let game = &mut ctx.accounts.game;
    let player = &mut ctx.accounts.player;
    let money_pot = &mut ctx.accounts.money_pot_vault;
    let owner = &mut ctx.accounts.owner;

    if secret_word == *word.secret_word {
        let lamports_to_transfer = money_pot.current_total * LAMPORTS_PER_SOL;

        let seeds = &[
            money_pot.to_account_info().key.as_ref(),
            "money-pot".as_bytes(),
            owner.to_account_info().key.as_ref(),
        ];

        let signer_seeds = &[&seeds[..], &[&[bump]]];

        invoke_signed(&transfer(&money_pot.key(), &owner.key(), lamports_to_transfer), &[
            money_pot.to_account_info(),
            owner.to_account_info(),
            ctx.accounts.system_program.to_account_info(),
        ], signer_seeds )?;

        game.total_games_won = game.total_games_won.checked_add(1).unwrap();
        player.games_won = player.games_won.checked_add(1).unwrap();
        player.total_winnings = player.total_winnings.checked_add(lamports_to_transfer).unwrap();
        money_pot.total_winnings = money_pot.total_winnings.checked_add(money_pot.current_total).unwrap();
        money_pot.current_total = 0;
        word.secret_word.clear();

        Ok(())
    } else {
        Err(GameErrors::SecretWordMismatch.into())
    }
}

test.ts

let [moneyVaultAcctPDA, bump] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("money-pot")],
      program.programId
  );
  console.log('moneyVaultAcct: ', moneyVaultAcctPDA.toString())

it( 'Player is able to claim money pot prize', async (): Promise<void> => {
      const secretWord = 'dog';
      const wordVault = await program.account.wordVault.fetch( wordVaultAcctPDA )
      console.log( 'Secret word is: ', wordVault.secretWord)

      const transaction = await program.methods.claimVictory( secretWord, bump )
          .accounts({
              game: gameAcctPDA,
              wordVault: wordVaultAcctPDA,
              player: playerAccount,
              moneyPotVault: moneyVaultAcctPDA,
              systemProgram: systemProgram
          })
          .signers([ ownerWallet.payer ])
          .rpc()

      console.log( 'Your transaction signature: ', transaction );

      const game = await program.account.gameData.fetch( gameAcctPDA )
      const player = await program.account.player.fetch( playerAccount )
      const moneyVault = await program.account.gamePot.fetch( moneyVaultAcctPDA )

      expect( game.totalGamesWon ).to.equal( 1 );
      expect( player.gamesWon ).to.equal( 1 );
      expect(wordVault.secretWord ).to.be.empty;
      expect( moneyVault.currentTotal.toNumber() ).to.equal( 0 );
  });

2 Answers 2

1

It looks like your money_pot_vault account is a custom GamePot account type from your program.

    #[account(mut)]
    pub money_pot_vault: Account<'info, GamePot>,

For the system_instruction::transfer instruction, the sender must be an account owned by the system program.

The transfer CPI is likely failing because the program owner of the money_pot_vault account is your program (and not the system program).

As part of the "Solana Account Model", only the program owner of an account is allowed to modify the data on the account (with the exception of adding lamports). This means you won't be able to use the System Program's transfer instruction to deduct lamports from the money_pot_vault account owned by your program.

Since the money_pot_vault is owned by your program, the solution is to update your instruction to directly modify the lamport balance on the account.

If you are using Anchor 0.28 or below, try:

 **ctx.accounts.money_pot_vault.try_borrow_mut_lamports()? -= lamports_to_transfer;
 **ctx.accounts.owner.try_borrow_mut_lamports()? += lamports_to_transfer;

If you are using Anchor 0.29, the syntax has been simplified to:

ctx.accounts.money_pot_vault.sub_lamports(lamports_to_transfer)?;
ctx.accounts.owner.add_lamports(lamports_to_transfer)?;

Here is the minimal example program from the Anchor repo

2
  • I'm closer. I added it this way because I already have variables for the ctx.accounts..........money_pot.sub_lamports(lamports_to_transfer)?; owner.add_lamports(lamports_to_transfer)?;...But I'm still getting the same error. When I comment out the transfer attempt and keep everything else, the test passes
    – Brian M.
    Dec 20, 2023 at 22:06
  • 1
    I figured it out. The issue was the amount I was passing in not matching the lamport balance in the account.
    – Brian M.
    Dec 21, 2023 at 17:53
0

I saw your code, and I think the problem could indeed be with the invocation. Test if the following code works. If it works, remember to give it a thumbs up, and if not, please provide more information about the code.

use crate::{
    errors::GameErrors,
    state::state::{GameData, GamePot, Player, WordVault},
};
use anchor_lang::{
    prelude::*,
    solana_program::{program::invoke_signed, system_instruction::transfer},
};
use solana_program::native_token::LAMPORTS_PER_SOL;

#[derive(Accounts)]
#[instruction(secret_word: String)]
pub struct ClaimVictory<'info> {
    #[account(mut)]
    pub game: Account<'info, GameData>,
    #[account(mut)]
    pub word_vault: Account<'info, WordVault>,
    #[account(mut)]
    pub player: Account<'info, Player>,
    #[account(mut)]
    pub money_pot_vault: Account<'info, GamePot>,
    #[account(mut)]
    pub owner: Signer<'info>,
    pub system_program: Program<'info, System>,
}

pub fn handle_claim_victory(
    ctx: Context<ClaimVictory>,
    secret_word: String,
    bump: u8,
) -> Result<()> {
    // get &mut accounts
    let word = &mut ctx.accounts.word_vault;
    let game = &mut ctx.accounts.game;
    let player = &mut ctx.accounts.player;
    let money_pot = &mut ctx.accounts.money_pot_vault;
    let owner = &mut ctx.accounts.owner;

    // validate secret word
    require!(
        secret_word == *word.secret_word,
        GameErrors::SecretWordMismatch
    );

    // transfer & tx in the blockchain
    let lamports_to_transfer: u64 = money_pot.current_total * LAMPORTS_PER_SOL;
    let seeds = &[
        money_pot.to_account_info().key.as_ref(),
        "money-pot".as_bytes(),
        owner.to_account_info().key.as_ref(),
    ];
    let signer_seeds = &[&seeds[..], &[&[bump]]];
    invoke_signed(
        &transfer(&money_pot.key(), &owner.key(), lamports_to_transfer),
        &[
            money_pot.to_account_info(),
            owner.to_account_info(),
            //ctx.accounts.system_program.to_account_info(),
        ],
        signer_seeds,
    )?;

    // update state
    game.total_games_won = game.total_games_won.checked_add(1).unwrap();
    player.games_won = player.games_won.checked_add(1).unwrap();
    player.total_winnings = player
        .total_winnings
        .checked_add(lamports_to_transfer)
        .unwrap();
    money_pot.total_winnings = money_pot
        .total_winnings
        .checked_add(money_pot.current_total)
        .unwrap();
    money_pot.current_total = 0;
    word.secret_word.clear();

    Ok(())
}

2
  • I like the idea of using require! over the if/else conditional, but I get the same result unfortunantly.
    – Brian M.
    Dec 20, 2023 at 22:18
  • I figured it out, I used John's suggestion. The issue was the amount I was passing in not matching the lamport balance in the account.
    – Brian M.
    Dec 21, 2023 at 17:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.