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?