1

Here's my Solana Program. It defines the logic for staking SPL tokens.

use anchor_lang::prelude::*;
use anchor_spl::{
    token::{TokenAccount, Mint,Token, Transfer, transfer, Approve},
    associated_token::{AssociatedToken, create, get_associated_token_address}
};
use std::mem::size_of;
// use solana_program::clock::Clock;

declare_id!("8eeWAxSis5uy5ox3ucNj2DouFsc7bfMvTnGx3wc1GaXG");

pub mod constants {
    pub const VAULT_SEED: &[u8] = b"vault";
    pub const STAKE_INFO_ACCOUNT: &[u8] = b"stake_info_account";
    pub const TOKEN_SEED: &[u8] = b"token_seed";
}

#[program]
pub mod staking_solana {
    use super::*;

    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }


    pub fn stake(ctx: Context<Stake>, amt:u64) -> Result<()> {
        let stake_info: &mut Account<'_, StakeInfo> = &mut ctx.accounts.stake_info_account; 

        if (stake_info.is_staked) {
            return Err(ErrorCodes::IsStaked.into());
        }

        if (amt == 0) {
            return Err(ErrorCodes::NoTokens.into());
        }

        // staking logic

        let clock  = &mut ctx.accounts.clock;

        stake_info.stake_at_slot = clock.slot;
        stake_info.is_staked = true;


        // format the staking amount
        // 10^decimals * amt
        let stake_amt = amt.checked_mul(10u64.pow(ctx.accounts.mint.decimals as u32)).unwrap();

       msg!("Transferring {} tokens from user_token_account ({}) to user_staking_account ({})", 
            stake_amt, 
            ctx.accounts.user_token_account.key(), 
            ctx.accounts.user_staking_ata.key() 
        );


        // transfer stake amt to user_Stake_account from user_token_account
        anchor_spl::token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.user_token_account.to_account_info(),
                    to: ctx.accounts.user_staking_ata.to_account_info(),
                    authority: ctx.accounts.signer.to_account_info(),
                }
            ),
            stake_amt 
        )?;
        
        msg!("balance of the user token account is {:?}", ctx.accounts.user_token_account.amount);
        msg!("balance of the user stake account is {:?}", ctx.accounts.user_staking_ata.amount);
            
        

        Ok(())
    }


    pub fn destake(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}


#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub signer : Signer<'info>,


    #[account(
        init_if_needed,
        seeds = [constants::VAULT_SEED],
        bump,
        payer = signer,
        token::mint = mint,
        token::authority = token_vault_account,
    )]
    pub token_vault_account : Account<'info, TokenAccount>,

    #[account()]
    pub mint: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Stake<'info> {
    #[account(mut)]
    pub signer : Signer<'info>,

    pub clock : Sysvar<'info, Clock>,

    #[account(
        init_if_needed,
        seeds = [constants::STAKE_INFO_ACCOUNT, signer.key.as_ref()],
        space = size_of::<StakeInfo>() + 8,
        bump,
        payer = signer,
    )]
    pub stake_info_account : Account<'info, StakeInfo>,

    #[account(
        init_if_needed,
        seeds = [constants::TOKEN_SEED, signer.key.as_ref()],
        bump,
        payer = signer,
        token::mint = mint,
        token::authority = user_staking_account
    )]
    pub user_staking_account : Account<'info, TokenAccount>,

    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = user_staking_account
    )]
    user_staking_ata : Account<'info, TokenAccount>,

    #[account(
        mut,
        associated_token::mint = mint,
        associated_token::authority = signer
    )]
    pub user_token_account : Account<'info, TokenAccount>,

    #[account()]
    pub mint: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program : Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct StakeInfo {
    pub total_staked: u64,
    pub remaining_rewards: u64,
    pub stake_at_slot: u64,
    pub is_staked: bool,
}

#[error_code]
pub enum ErrorCodes{
    #[msg("Tokens already staked")]
    IsStaked,
    #[msg("Tokens not staked")]
    NotStaked,
    #[msg("No Tokens To Stake")]
    NoTokens
}

There are 5 different accounts involved illustrated below

5 Accounts Involved

The SPL tokens are supposed to be sent from User's Token Account to User STaking Info Acc.

Here's the TS test suite for this

  it("stake", async () => {
    // Create user account
    const local_connection = new Connection("http://localhost:8899", {
      commitment: "confirmed",
    });

    console.log("mint is:", mint);

    const user_addr = Keypair.generate();

    // creating ATA to the user (user is the owner of token account)
    const user = await getOrCreateAssociatedTokenAccount(
      local_connection,
      payer.payer,
      mint,
      payer.publicKey // Correct owner of token account is the user
    );

    console.log("user token account is ", user.address);

    // Mint some tokens to the user (e.g., 100 tokens)
    await mintTo(
      local_connection,
      payer.payer,
      mint,
      user.address, // Correct user token account
      payer.publicKey, // payer is the owner of the mint
      1e11, // This mints 100 tokens (1e11 atomic units with 9 decimals)
      [payer.payer]
    );

    // Get the user's initial token balance before staking
    const initialUserBalance = await getTokenBalance(
      local_connection,
      mint,
      payer.publicKey // Use the correct user token account
    );
    console.log("Initial User Token Balance:", initialUserBalance);

    // Get the stake info account for the program
    const [stake_info] = PublicKey.findProgramAddressSync(
      [Buffer.from("stake_info_account"), payer.publicKey.toBuffer()],
      program.programId
    );

    // Get the user_stake PDA (program-derived address)
    const [stake_account] = PublicKey.findProgramAddressSync(
      [Buffer.from("token_seed"), payer.publicKey.toBuffer()], // Ensure the correct user address is used for PDA derivation
      program.programId
    );

    // Create an associated token account for the user_stake PDA (program owns the stake account)
    const stake_acc_ata = await getOrCreateAssociatedTokenAccount(
      local_connection,
      payer.payer,
      mint,
      stake_account, // Stake account owned by the program's PDA
      true // This ensures it creates the ATA for the program's PDA
    );
    console.log("stake ATA is ", stake_acc_ata.address);
    console.log("user token account is ", user.address);

    // Staking 1 token (1e9 in atomic units, considering 9 decimals)
    const amountToStake = new anchor.BN(1); // This represents 1 token

    // Execute the staking transaction
    const tx = await program.methods
      .stake(amountToStake) // Assuming 1 token is being staked
      .signers([payer.payer])
      .accountsPartial({
        stakeInfoAccount: stake_info,
        userStakingAccount: stake_account,
        userTokenAccount: user.address, // Use the correct user token account
        mint: mint,
        signer: payer.publicKey, // The signer should be the user staking the tokens
      })
      .rpc();

    console.log("Transaction signature:", tx);

    // Get the user's token balance after staking
    const token_amt = (await getAccount(local_connection, user.address)).amount;
    console.log("Final User Token Balance:", token_amt);

    // Check that the user's balance was reduced by the staked amount (1 token)
    assert.equal(
      BigInt(initialUserBalance) - token_amt,
      BigInt(1e9), // 1 token in atomic units (with 9 decimals)
      "User's token balance should decrease by 1 token"
    );

    // Get the staked account balance to verify the staked amount
    const stakedAccountBalance = await getTokenBalance(
      local_connection,
      mint,
      stake_acc_ata.address // Check the stake account's balance
    );
    console.log("Staked Account Balance:", stakedAccountBalance);

    // Check that the staked account has the staked tokens (1 token)
    assert.equal(
      BigInt(stakedAccountBalance),
      BigInt(1e9), // 1 token in atomic units (with 9 decimals)
      "Staked account should have the correct token amount (1 token)"
    );
  });

There's NO issue in the function execution; just that after the tx when we check the balance of the user's staking info account's Token account; it returns a balance of 0.

I am a newbie in Solana dev, so forgive me for asking something that may be obvious!!

1 Answer 1

0

Your program should have only one ATA owned by one PDA for a mint

The program includes a vault(PDA), which will serve as the owner of the vault_ata (TokenAccount) and are initialized once.

pub mod constants {
    pub const VAULT_SEED: &[u8] = b"vault";
    pub const STAKE_INFO_ACCOUNT: &[u8] = b"stake_info_account";
}

#[program]
pub mod staking_solana {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let vault = &mut ctx.accounts.vault;

        vault.total_staked_amount = 0u64;

        Ok(())
    }

    pub fn stake(ctx: Context<Stake>, amt: u64) -> Result<()> {
        let stake_info = &mut ctx.accounts.stake_info_account;

        if (stake_info.is_staked) {
            return Err(ErrorCodes::IsStaked.into());
        }

        if (amt == 0) {
            return Err(ErrorCodes::NoTokens.into());
        }

        // staking logic

        let clock = Clock::get()?;

        stake_info.stake_at_slot = clock.slot;
        stake_info.is_staked = true;
        stake_info.total_staked += amt;

        // keep track of global total stake amount
        let vault = &mut ctx.accounts.vault;
            vault += amt;

        // no need to check decimals inside the program

        msg!(
            "Transferring {} tokens from user_token_account ({}) to vault_account ({})",
            amt,
            ctx.accounts.user_token_account.key(),
            ctx.accounts.vault_ata.key()
        );

        // transfer stake amt to vault_ata from user_token_account
        anchor_spl::token::transfer_checked(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                TransferChecked {
                    from: ctx.accounts.user_token_account.to_account_info(),
                    to: ctx.accounts.vault_ata.to_account_info(),
                    authority: ctx.accounts.signer.to_account_info(),
                    mint: ctx.accounts.mint.to_account_info(),
                },
            ),
            amt,
            ctx.accounts.mint.decimals,
        )?;

        msg!(
            "balance of the user token account is {:?}",
            ctx.accounts.user_token_account.amount
        );
        msg!(
            "balance of the user stake account is {:?}",
            stake_info.total_staked
        );

        Ok(())
    }

    pub fn destake(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        seeds=[constants::VAULT_SEED],
        payer=signer,
        bump,
        space=size_of::<Vault>() + 8
    )]
    pub vault: Account<'info, Vault>,

    #[account(
        init,
        payer=signer,
        associated_token::mint=mint,
        associated_token::authority=vault,
    )]
    pub vault_ata: Account<'info, TokenAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,

    #[account()]
    pub mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub associated_token_account: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Stake<'info> {
    #[account(
        mut,
        seeds=[constants::VAULT_SEED],
        bump,
    )]
    pub vault: Account<'info, Vault>,

    #[account(
        mut,
        associated_token::mint = mint,
        associated_token::authority = vault
    )]
    vault_ata: Account<'info, TokenAccount>,

    #[account(
        init_if_needed,
        seeds = [constants::STAKE_INFO_ACCOUNT, signer.key.as_ref()],
        space = size_of::<StakeInfo>() + 8,
        bump,
        payer = signer,
    )]
    pub stake_info_account: Account<'info, StakeInfo>,

    #[account(
        mut,
        associated_token::mint = mint,
        associated_token::authority = signer
    )]
    pub user_token_account: Account<'info, TokenAccount>,

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

    #[account()]
    pub mint: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct Vault {
    pub total_staked_amount: u64,
}

#[account]
pub struct StakeInfo {
    pub total_staked: u64,
    pub remaining_rewards: u64,
    pub stake_at_slot: u64,
    pub is_staked: bool,
}

#[error_code]
pub enum ErrorCodes {
    #[msg("Tokens already staked")]
    IsStaked,
    #[msg("Tokens not staked")]
    NotStaked,
    #[msg("No Tokens To Stake")]
    NoTokens,
}

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.