0

I'm working on a Solana program using Anchor where I mint tokens to a vault and am building a sell_tokens function to sell SPL tokens at a fixed rate. Below is the client-side code for selling tokens:

  const sellTokens = async (
    mint: PublicKey,
    tokenVault: PublicKey,
    vaultPda: PublicKey
  ) => {
    const user = wallet?.publicKey;

    try {
      if (!wallet || !user) {
        throw new WalletNotConnectedError();
      }

      const amount = 1000000000;

      // Derive the associated token account address
      const userTokenAccount = await getAssociatedTokenAddress(mint, user);
      console.log('User token account address:', userTokenAccount.toBase58());

      const accountInfo = await connection.getAccountInfo(userTokenAccount);
      if (!accountInfo) {
        await createAssociatedTokenAccount(
          user,
          userTokenAccount,
          mint,
          wallet
        );
      } else {
        console.log('Token account already exists');
      }

      await program?.methods
        .sellTokens(new anchor.BN(amount))
        .accounts({
          mint: mint,
          tokenVault: tokenVault,
          userTokenAccount: userTokenAccount,
          vaultPda: vaultPda,
          user: user,
          systemProgram: SystemProgram.programId,
          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
          tokenProgram: TOKEN_PROGRAM_ID,
        })
        .rpc();

      console.log('Tokens sold successfully');
    } catch (e) {
      console.error('Error in sellTokens:', e);
    }
  };

From the code above, it seems to create the associated token account if it does not exist correctly. But when I trigger my sell_tokens function on my program, I get the following error:

{
    "logs": [
        "Program ComputeBudget111111111111111111111111111111 invoke [1]",
        "Program ComputeBudget111111111111111111111111111111 success",
        "Program ComputeBudget111111111111111111111111111111 invoke [1]",
        "Program ComputeBudget111111111111111111111111111111 success",
        "Program 62WsnnUTkyeopn5iRqz9GWMauss6TRQsZXXQo9qRcXDq invoke [1]",
        "Program log: Instruction: SellTokens",
        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
        "Program log: Instruction: Transfer",
        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 186388 compute units",
        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
        "Program 62WsnnUTkyeopn5iRqz9GWMauss6TRQsZXXQo9qRcXDq consumed 18699 of 199700 compute units",
        "Program 62WsnnUTkyeopn5iRqz9GWMauss6TRQsZXXQo9qRcXDq failed: instruction changed the balance of a read-only account"
    ],
    "programErrorStack": [
        "62WsnnUTkyeopn5iRqz9GWMauss6TRQsZXXQo9qRcXDq"
    ]
}

It says "failed to send transaction: Transaction simulation failed: Error processing Instruction 2: instruction changed the balance of a read-only account", but looking at my Rust program, it looks like I've set the accounts to be mutable.

Here is the Rust code for the sell_tokens function and context:

    pub fn sell_tokens(ctx: Context<SellTokens>, amount: u64) -> Result<()> {
        let exchange_rate = 10_000; // 10,000 tokens per 1 SOL
        let sol_amount = amount * 1_000_000_000 / exchange_rate; // Calculate SOL amount in lamports

        // Transfer tokens from vault to user
        let transfer_instruction = Transfer {
            from: ctx.accounts.token_vault.to_account_info(),
            to: ctx.accounts.user_token_account.to_account_info(),
            authority: ctx.accounts.vault_pda.to_account_info(),
        };

        let user_key = ctx.accounts.user.key();
        let mint_key = ctx.accounts.mint.key();
        let vault_bump = ctx.bumps.vault_pda;
        let seeds = &[b"vault-authority", user_key.as_ref(), mint_key.as_ref(), &[vault_bump]];
        let signer = &[&seeds[..]];

        let cpi_ctx = CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            transfer_instruction,
            signer,
        );

        token::transfer(cpi_ctx, amount)?;

        // Transfer SOL from user to vault's native SOL account
        **ctx.accounts.user.try_borrow_mut_lamports()? -= sol_amount;
        **ctx.accounts.vault_pda.try_borrow_mut_lamports()? += sol_amount;
        Ok(())
    }

#[derive(Accounts)]
pub struct SellTokens<'info> {
    #[account(mut)]
    pub mint: Account<'info, Mint>,

    #[account(
        mut,
        token::mint = mint,
        token::authority = vault_pda,
        seeds = [b"vault", user.key().as_ref(), mint.key().as_ref(),],
        bump
    )]
    pub token_vault: Account<'info, TokenAccount>,

    #[account(mut)]
    user_token_account: Account<'info, TokenAccount>,

    #[account(
        mut,
        seeds = [b"vault-authority", user.key().as_ref(), mint.key().as_ref(), ],
        bump
    )]
    pub vault_pda: AccountInfo<'info>,

    #[account(mut)]
    pub user: Signer<'info>,

    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    pub token_program: Program<'info, Token>,
}

It looks like I've set the accounts that I'll be transferring tokens to/from as (mut) (token_vault, user_token_account, vault_pda, and user).

Why am I getting the "instruction changed the balance of a read-only account" error, and how can I fix it?

1 Answer 1

1

I think this line is causing all your problems

        **ctx.accounts.user.try_borrow_mut_lamports()? -= sol_amount;

You are trying to deduct lamports from an account owned by the system program and not your program.

Use the transfer instruction if you want to deduct lamports from an account you do not own but have authority over

use anchor_lang::{
    prelude::*,
    system_program::{Transfer as SOLTransfer},
};

let seller_cpi_accounts = SOLTransfer {
    from: ctx.accounts.user.to_account_info(),
    to: ctx.accounts.vault_pda.to_account_info(),
};

Note: you can only deduct lamports for accounts that your program/account owns

Adding is opposite as any account can transfer lamports to any address.

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.