10

I am using anchor and I need to check that two different PDAs have not been initialized. Later on in a different function call I need to check if a PDA has been initialized before closing the account.

How do I check this without using anchor account constraints?

For the NOT initialized part, I have this:

...
#[account(
  zero,
  seeds = [
    b"some-seed",
    partner0_pub.key().as_ref(),
  ],
  bump,
)]
/// CHECK: its fine...
pub example_pda0: AccountInfo<'info
#[account(
  zero,
  seeds = [
    b"some-seed",
    partner1_pub.key().as_ref(),
  ],
  bump,
)]
/// CHECK: its fine...
pub example_pda1: AccountInfo<'info
...

Does the zero constraint do what I think it does here?

For the second part, I am not sure what I can do here... I'm sure there is a lower level call that I could probably make but I didn't immediately find it...

4 Answers 4

8

Without constraints

Check initialized

Account::try_from will check

  • the account is owned by the correct program
  • the account is initialized
  • the account has the correct discriminator and can be deserialized into the struct
let example_pda1_account_info = ctx.accounts.example_pda1.to_account_info()
let mut example_pda1: Account<ExamplePda> = 
   Account::try_from(example_pda1_account_info)?;

Check uninitialized

I haven't checked that this actually compiles but this is adapted from the zero discriminator check that the zero constraint source code uses.

let mut __data: &[u8] = ctx.accounts.example_pda1.to_account_info().try_borrow_data()?;
let mut __disc_bytes = [0u8; 8];
__disc_bytes.copy_from_slice(&__data[..8]);
let __discriminator = u64::from_le_bytes(__disc_bytes);
if __discriminator != 0 {
    return Err();
}

With constraints

Check initialized

To check that a program account has been initialized you can just get anchor to try and load the program account in the accounts context, similar to how you verify the account is empty above.

#[account(
  seeds = [
    b"some-seed",
    partner1_pub.key().as_ref(),
  ],
  bump = example_pda1.bump,
)]
pub example_pda1: Account<'info, ExamplePdaAccount>

Anchor will try to load the account data and check that the 8 bytes at the start of the account match the expected discriminator.

Check uninitialized

For checking that an account is not initialized I would instead check that the system program still owns the account. A system program account can't have data on it, though it might have some lamports in it.

#[account(
  seeds = [
    b"some-seed",
    partner1_pub.key().as_ref(),
  ],
  bump,
)]
pub example_pda1: SystemAccount<'info>

Finally zero doesn't do what you expect it to do here. It does a lot more. Check out the constraints doc https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html#constraints. Essentially it checks that the 8 byte discriminator is not initialized and that the account has enough solana rent to be rent exempt (although this is now a run time requirement) but it will also check that your account is owned by the program calling the function, ensures the account is mutable, and initializes the account + the discriminator. zero is essentially a lighter weight version of init that needs to be called in different scenarios that are explained in the docs.

Use this constraint (zero) if you want to create an account in a previous instruction and then initialize it in your instruction instead of using init. This is necessary for accounts that are larger than 10 Kibibyte because those accounts cannot be created via a CPI (which is what init would do).

3
  • really nice answer but I was looking to do this from outside the constraints. Thanks for the help though! Jul 20, 2022 at 11:32
  • Oh right, I missed that part of the question
    – Henry E
    Jul 20, 2022 at 13:06
  • Ok, updated the answer with how to do it outside of constraints as well. I would recommend just using constraints though if possible
    – Henry E
    Jul 20, 2022 at 13:22
3

What "(de)initialized" looks like is up to your program. Typically it's a some combination of the assigned owner program, lamports balance, allocated data size and/or value(s) stored in the data.

For instance the native programs typically assert that the account has been assigned to them, has the expected (or at least sufficient space allocated), enough lamports to be rent-exempt and the value of a "state" and/or "version" enum which wraps all stored data

1
  • These all make sense. It would be great if you could show me some examples of these in use though. Jul 19, 2022 at 19:22
1

There may be a better way to check if an account has been initialised, but personally I just check if it has lamports:

if **destination_account_info.try_borrow_lamports()? > 0 {
        msg!("Confirmed destination account is already initialised.");
}
1
  • This idea crossed my mind. Would you happen to know how to check for any data in the account? I think with the two combined, it would be a pretty safe bet… Jul 19, 2022 at 17:57
1

Everyone here posted pretty good answers but without code to back it up, so I will probably end up going with my own answer now that I have figured this issue out.

I realized that AccountInfo holds basically all of the information that you need. You can check on the account balance, data, and ownership from there, which makes sense.

Here is an example function for checking that an account is not yet initialized. I think readers can figure out how to do the inverse based on this function alone:

fn check_valid_partner(
    &self,
    account: &UncheckedAccount,
    program_id: &Pubkey,
) -> Result<()> {
    let a = account.to_account_info();

    // partner PDA should not have any data yet
    let data_empty = a.data_is_empty();
    if !data_empty {
        return Err(MyProgErr::PartnerDataNotEmpty.into());
    }

    // partner PDA should not have rent paid yet
    let lamps = a.lamports();
    let has_lamps = lamps > 0;
    if has_lamps {
        return Err(MyProgErr::PartnerBalanceNotZero.into());
    }

    // program should should not have ownership yet
    let pda_owned_by_prog = a.owner == program_id;
    if pda_owned_by_prog {
        return Err(MyProgErr::PartnerAlreadyOwned.into());
    }

    Ok(())
}

Using the Result type plays nicely with instructions where you can use the ? operator like this:

check_valid_partner(&ctx.accounts.partner0, ctx.program_id)?;

The accounts that need to be check can be passed the Accounts struct as UncheckedAccounts since it seems that most of the stuff being done here is a bit lower level. This would look something like this:

/// CHECK: we are doing all needed checks manually on this account
pub partner0: UncheckedAccount<'info>,
#[account(
    seeds = [
        b"partner",
        partner1_pub.key().as_ref(),
    ],
    bump,
)]

I will upvote all other answers as of now since they, at the very least, provided inspiration for my own answer.

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.