[derive(Accounts)] in Anchor: different kinds of accounts

#[derive(Accounts)] in Solana Anchor is an attribute-like macro for structs that holds references to all the accounts the function will access during its execution.

In Solana, every account the transaction will access must be specified in advance

One reason Solana is so fast is that it executes transactions in parallel. That is, if Alice and Bob both want to do a transaction, Solana will try to process their transactions simultaneously. However there is a problem if their transactions conflict by accessing the same storage. For example, suppose both Alice and Bob are trying to write to the same account. Clearly their transactions cannot be run in parallel.

For Solana to know that Alice and Bob’s transaction cannot be parallelized, both Alice and Bob must specify in advance all the accounts their transaction will update.

Since both Alice and Bob specify a (storage) account, the Solana runtime can infer that both transactions conflict. One must be chosen (presumably, the one that paid the higher priority fee), and the other will end up failing.

This is why each function has it’s own separate #[derive(Accounts)] struct. Each field in the struct is an account that the program intends to (but is not required to) access during execution.

Some Ethereum developers may notice the similarity with this requirement and EIP 2930 access list transactions.

The type of account signals to Anchor how you intend to interact with the account.

Account types you will use most frequently: Account, Unchecked Account, System Program, and Signer

In our code to initialize storage, we saw three different “kinds” of accounts:

  • Account
  • Signer
  • Program

Here is the code again:

Account types code snippet

And when we read an account balance, we saw a fourth type:

  • UncheckedAccount

Here is the code we used: code for unchecked account

Each of the items we highlighted with the green boxes are imported via the anchor_lang::prelude::*; at the top of the files.

The purpose of Account, UncheckedAccount, Signer, and Program are to perform some kind of a check on the account passed in before proceeding, and also to expose functions for interacting with those accounts.

We will further explain each of these four types in the following sections.

Account

The Account type will check that the owner of the account being loaded is actually owned by the program. If the owner does not match, then it won’t load. This serves as an important safety measure to not accidentally read in data the program did not create.

In the following example, we create a keypair account and try to pass it to foo. Because the account is not owned by the program, the transaction fails.

Rust:

use anchor_lang::prelude::*;

declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs");

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

    pub fn foo(ctx: Context<Foo>) -> Result<()> {        
        // we don't do anything with the account SomeAccount        
        Ok(())    
    }
}

#[derive(Accounts)]
pub struct Foo<'info> {    
    some_account: Account<'info, SomeAccount>,
}

#[account]
pub struct SomeAccount {}

Typescript:


import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { AccountTypes } from "../target/types/account_types";

describe("account_types", () => {
    async function airdropSol(publicKey, amount) {    
        let airdropTx = await anchor
            .getProvider()
            .connection.requestAirdrop(
                publicKey, 
                amount * anchor.web3.LAMPORTS_PER_SOL
            );  

        await confirmTransaction(airdropTx);  
    }  

    async function confirmTransaction(tx) {    
        const latestBlockHash = await anchor
            .getProvider()
            .connection.getLatestBlockhash();

        await anchor
            .getProvider()
            .connection.confirmTransaction({      
                blockhash: latestBlockHash.blockhash,      	
                lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,      
                signature: tx,    
        });  
    }  

    // Configure the client to use the local cluster.  
    anchor.setProvider(anchor.AnchorProvider.env());  

    const program = anchor.workspace.AccountTypes as Program<AccountTypes>;  

    it("Wrong owner with Account", async () => {    
        const newKeypair = anchor.web3.Keypair.generate();    
        await airdropSol(newKeypair.publicKey, 10);    

        await program.methods
        .foo()
        .accounts({someAccount: newKeypair
        .publicKey}).rpc();  
    });
});

Here is the output from executing the tests:

test execution output

If we add an init macro to Account, then it will try to transfer ownership from the system program to this program. However, the code above does not have an init macro.

More about the Account type can be found in the docs: https://docs.rs/anchor-lang/latest/anchor_lang/accounts/account/struct.Account.html

UncheckedAccount or AccountInfo

UncheckedAccount is an alias for AccountInfo. This does not check for ownership, so care must be taken as it will accept arbitrary accounts.

Here is an example of using UncheckedAccount to read the data of an account it does not own.

use anchor_lang::prelude::*;

declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs");

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

    pub fn foo(ctx: Context<Foo>) -> Result<()> {        
        let data = &ctx.accounts.some_account.try_borrow_data()?;        
        msg!("{:?}", data);        
        Ok(())    
    }
}

#[derive(Accounts)]
pub struct Foo<'info> {    
    /// CHECK: we are just printing the data    
    some_account: AccountInfo<'info>,
}

Here is our Typescript code. Note that we are calling the system program directly to create the keypair account so that we can allocate 16 bytes of data.


import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { AccountTypes } from "../target/types/account_types";

describe("account_types", () => {  
    const wallet = anchor.workspace.AccountTypes.provider.wallet;  
	
    // Configure the client to use the local cluster.  
    anchor.setProvider(anchor.AnchorProvider.env());  

    const program = anchor.workspace.AccountTypes as Program<AccountTypes>;  
    it("Load account with accountInfo", async () => {    
        // CREATE AN ACCOUNT NOT OWNED BY THE PROGRAM    
        const newKeypair = anchor.web3.Keypair.generate();    
        const tx = new anchor.web3.Transaction().add(      
            anchor.web3.SystemProgram.createAccount({        
                fromPubkey: wallet.publicKey,        
                newAccountPubkey: newKeypair.publicKey,        
                space: 16,        
                lamports: await anchor          
                    .getProvider()          				
                    .connection
                    .getMinimumBalanceForRentExemption(32),        		
                programId: program.programId,      
            })    
	);    

	await anchor.web3.sendAndConfirmTransaction(      
            anchor.getProvider().connection,      
            tx,      
            [wallet.payer, newKeypair]    
	);    

	// READ THE DATA IN THE ACCOUNT    
	await program.methods      
            .foo()      
            .accounts({ someAccount: newKeypair.publicKey })      
            .rpc();  
    });
});

After the program runs, we can see it printed out the data in the account, which holds 16 zero bytes:

Transaction executed in slot 14298:
  Signature: 64fv6NqYB4tji9UfLpH8PgFDY1QV4vbMovrnnpw3271vStg7J5g1z1bm9YbE8Lobzozkc6y2YzLdgMjGdftCGKqv
  Status: Ok
  Log Messages:
    Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs invoke [1]
    Program log: Instruction: Foo
    Program log: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs consumed 5334 of 200000 compute units
    Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs success

We needed to use this account type when we were passing in an arbitrary address, but be very careful how the data is used, because a hacker may be able to craft malicious data in an account then pass that to the Solana program.

Signer

This type will check that the Signer account signed the transaction; it checks that the signature matches the public key of the account.

Because a signer is also an account, you can read the Signer’s balance or data (if any) stored in the account, though it’s primary purpose is to validate signatures.

According to the docs https://docs.rs/anchor-lang/latest/anchor_lang/accounts/signer/struct.Signer.html, Signer is a type validating that the account signed the transaction. No other ownership or type checks are done. If this is used, one should not try to access the underlying account data.

Rust example:

use anchor_lang::prelude::*;

declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs");#

[program]
pub mod account_types {    
    use super::*;    
    pub fn hello(ctx: Context<Hello>) -> Result<()> {        
        let lamports = ctx.accounts.signer.lamports();        
        let address = &ctx.accounts
            .signer
            .signer_key().unwrap();        
        msg!(
            "hello {:?} you have {} lamports", 
            address, 
            lamports
        );        
        Ok(())    
    }
}

#[derive(Accounts)]
pub struct Hello<'info> {    
    pub signer: Signer<'info>,
}

Typescript:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { AccountTypes } from "../target/types/account_types";

describe("account_types", () => {  
    anchor.setProvider(anchor.AnchorProvider.env()); 

    const program = anchor.workspace.AccountTypes as Program<AccountTypes>;  

    it("Wrong owner with Account", async () => {    
        await program.methods.hello().rpc();  
    });
});

Here is the output of the program:

Transaction executed in slot 11184:
  Signature: 4xipobKHHp7a3N4durXN4YPGUesDAJNg7wsatBemdJAm7U1dXYG3gveLwnuY39iCTEZvaj6nnAViVJwDS8124uJJ
  Status: Ok
  Log Messages:
    Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs invoke [1]
    Program log: Instruction: Hello
    Program log: hello 5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj you have 499999994602666000 lamports
    Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs consumed 13096 of 200000 compute units
    Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs success

Program

This should be self explanatory. It signals to Anchor the account is an executable one, i.e. a program, and you may issue to it a cross program invocation. The one we have been using is the system program, though later we will use our own programs.

Learn more

Learn Solana development in our Ethereum to Solana course.

Originally Published April, 6, 2024

Cross Program Invocation In Anchor

Cross Program Invocation In Anchor Cross Program Invocation (CPI) is Solana’s terminology for a program calling the public function of another program. We’ve already done CPI before when we sent a transfer SOL transaction to the system program. Here is the relevant snippet by way of reminder: pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> […]

Reading Another Anchor Program’s Account Data On Chain

Reading Another Anchor Program’s Account Data On Chain In Solidity, reading another contract’s storage requires calling a view function or the storage variable being public. In Solana, an off-chain client can read a storage account directly. This tutorial shows how an on-chain Solana program can read the data in an account it does not own. […]

Modifying accounts using different signers

Modifying accounts using different signers In our Solana tutorials thus far, we’ve only had one account initialize and write to the account. In practice, this is very restrictive. For example, if user Alice is transferring points to Bob, Alice must be able to write to an account initialized by user Bob. In this tutorial we […]

Deleting and Closing Accounts and Programs in Solana

Deleting and Closing Accounts and Programs in Solana In the Anchor framework for Solana, close is the opposite of init (initializing an account in Anchor) — it reduces the lamport balance to zero, sending the lamports to a target address, and changes the owner of the account to be the system program. Here is an […]