1

I was following the Token Vault tutorial on Solana Playground. It compiled fine on SolPG but when I compile this program on my system, I get the following error about unsafe account. I want to make this account safe and not just mute the error by adding the comment. Please guide. Thank you.

Error

Struct field "token_account_owner_pda" is unsafe, but is not documented.
        Please add a `/// CHECK:` doc comment explaining why no checks through types are necessary.
        See https://www.anchor-lang.com/docs/the-accounts-struct#safety-checks for more information.

Program

use anchor_lang::prelude::*;
use anchor_spl::token::{transfer, Mint, Token, TokenAccount, Transfer};

// This is your program's public key and it will update
// automatically when you build the project.
declare_id!("HB5oC3fNVdJji92n1iaQBXVQiccRBvHz7e5jiMnLT4F8");

#[program]
mod token_vault {
    use super::*;
    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }

    pub fn stake(ctx: Context<Stake>, amount: u64) -> Result<()> {
        msg!("Stake amount: {}!", amount);

        let stake_info = &mut ctx.accounts.stake_info;
        stake_info.amount = amount;
        stake_info.staker = ctx.accounts.signer.key();

        let transfer_instruction = Transfer {
            from: ctx.accounts.sender_token_account.to_account_info(),
            to: ctx.accounts.vault_token_account.to_account_info(),
            authority: ctx.accounts.signer.to_account_info(),
        };

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

        transfer(cpi_ctx, amount)?;

        Ok(())
    }

    pub fn destake(ctx: Context<DeStake>, amount: u64) -> Result<()> {
        msg!("Destake amount: {}!", amount);

        let stake_info = &mut ctx.accounts.stake_info;

        if amount == 0 {
            return Err(error!(ErrorCode::AmountZero));
        }

        if amount > stake_info.amount {
            return Err(error!(ErrorCode::WithdrawAmountGreater));
        }

        if stake_info.amount == 0 {
            return Err(error!(ErrorCode::StakeAmountZero));
        }

        let updated_amount = stake_info.amount.checked_sub(amount).unwrap();
        ctx.accounts.stake_info.amount = updated_amount;

        let transfer_instruction = Transfer {
            from: ctx.accounts.vault_token_account.to_account_info(),
            to: ctx.accounts.sender_token_account.to_account_info(),
            authority: ctx.accounts.token_account_owner_pda.to_account_info(),
        };

        let bump = ctx.bumps.token_account_owner_pda;
        let seeds = &[b"token_account_owner_pda".as_ref(), &[bump]];
        let signer = &[&seeds[..]];

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

        transfer(cpi_ctx, amount)?;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    // Derived PDAs
    #[account(
        init_if_needed,
        payer = signer,
        seeds=[b"token_account_owner_pda"],
        bump,
        space = 8
    )]
    token_account_owner_pda: AccountInfo<'info>,

    #[account(
        init_if_needed,
        payer = signer,
        seeds=[b"token_vault", mint_of_token_being_sent.key().as_ref()],
        token::mint=mint_of_token_being_sent,
        token::authority=token_account_owner_pda,
        bump
    )]
    vault_token_account: Account<'info, TokenAccount>,

    mint_of_token_being_sent: Account<'info, Mint>,

    #[account(mut)]
    signer: Signer<'info>,
    system_program: Program<'info, System>,
    token_program: Program<'info, Token>,
    rent: Sysvar<'info, Rent>,
}

#[derive(Accounts)]
pub struct Stake<'info> {
    // Derived PDAs
    #[account(
        init_if_needed,
        payer = signer,
        seeds=[b"stake_info", signer.key().as_ref()],
        bump,
        space = 8 + StakeInfo::INIT_SPACE
    )]
    pub stake_info: Account<'info, StakeInfo>,

    #[account(mut,
        seeds=[b"token_account_owner_pda"],
        bump
    )]
    token_account_owner_pda: AccountInfo<'info>,

    #[account(mut,
        seeds=[b"token_vault", mint_of_token_being_sent.key().as_ref()],
        bump,
        token::mint=mint_of_token_being_sent,
        token::authority=token_account_owner_pda,
    )]
    vault_token_account: Account<'info, TokenAccount>,

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

    mint_of_token_being_sent: Account<'info, Mint>,

    #[account(mut)]
    signer: Signer<'info>,
    system_program: Program<'info, System>,
    token_program: Program<'info, Token>,
    rent: Sysvar<'info, Rent>,
}

#[derive(Accounts)]
pub struct DeStake<'info> {
    // Derived PDAs
    #[account(
        mut,
        seeds=[b"stake_info", signer.key().as_ref()],
        bump,
    )]
    pub stake_info: Account<'info, StakeInfo>,

    #[account(mut,
        seeds=[b"token_account_owner_pda"],
        bump
    )]
    token_account_owner_pda: AccountInfo<'info>,

    #[account(mut,
        seeds=[b"token_vault", mint_of_token_being_sent.key().as_ref()],
        bump,
        token::mint=mint_of_token_being_sent,
        token::authority=token_account_owner_pda,
    )]
    vault_token_account: Account<'info, TokenAccount>,

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

    mint_of_token_being_sent: Account<'info, Mint>,

    #[account(mut)]
    signer: Signer<'info>,
    system_program: Program<'info, System>,
    token_program: Program<'info, Token>,
    rent: Sysvar<'info, Rent>,
}

#[account]
#[derive(InitSpace)]
pub struct StakeInfo {
    pub staker: Pubkey,
    pub amount: u64,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Amount must be greater than zero")]
    AmountZero,

    #[msg("Withdraw amount must be less than the deposit")]
    WithdrawAmountGreater,

    #[msg("Deposit amount is zero")]
    StakeAmountZero,
}

2 Answers 2

2

Two ways to fix this - either add the comment, or make it a more specific account instead of an AccountInfo. In your example, this is a special account of length 0, so first declare a new Account type for it in your program somewhere:

#[account]
pub struct TokenAccOwnerAccount {}

And then define your instruction accordingly:

    #[account(
        init_if_needed,
        payer = signer,
        seeds=[b"token_account_owner_pda"],
        bump,
        space = 8
    )]
    token_account_owner_pda: Account<'info, TokenAccOwnerAccount>,
2
  • This definitely silences the error. But is it safe though?
    – Anas
    Commented Jun 6 at 19:56
  • Yes. The error is basically saying it's unsafe bc you're not defining what account format to pass, meaning in theory anyone could pass any account as long as it satisfies the other constraints above. By defining it, you're telling anchor to error if it has the wrong format (in practice discriminator), effectively making it safe.
    – McBain
    Commented Jun 6 at 21:34
1

TL;DR; this usage is safe and you are expected to add the /// CHECK: comment at the field.

To understand the error, let's break it down:

Struct field "token_account_owner_pda" is unsafe, but is not documented.

Consult the documentation here for more details: Safety Checks.

In Solana contract programming, at least 50% of the effort involves checking constraints. The Anchor framework helps by managing these checks in the background, ensuring you don’t miss any.

Anchor encourages using the Account (like Account<'info, Mint>). This allows Anchor to verify the account's existence, ownership by the correct Program ID, and correct data structure.

Using AccountInfo instead means Anchor cannot verify the account/data.

Be aware that any account/data could be passed into your on-chain program. You must discard any incorrect accounts and their data in the on-chain program code. Solana does not help here at all, but Anchor and its macros may.

Therefore, Anchor suggests you double-check your work, ensure all checks are in place, and add /// CHECK: to fields to indicate you're aware of their safety requirements.

There is an ongoing discussion about removing AccountInfo from the framework in favour of UncheckedAccount to emphasize that these accounts are unchecked. For more details, see GitHub issue #2794.

While it is alright to use AccountInfo, it's nothing bad with it.

In many cases, it's appropriate to use AccountInfo/UncheckedAccount (considering them equivalent). For example, if you pass a non-existent PDA used solely as an authority, you don’t need the account to exist.

(Here we can consider using init_if_needed as not a good idea.)

For the case of the PDA the other defined constraints on the AccountInfo cover all relevant checks - Anchor checks the address and the owner (being the current program itself). And thus this is safe.

    #[account(mut,
        seeds=[b"token_account_owner_pda"],
        bump
    )]
    token_account_owner_pda: AccountInfo<'info>,

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.