Create your own
Lesson illustration

Securing Accounts with Anchor Constraints

Hello! Welcome back to our module on Auditing and Optimizing Solana Programs.

In the last lesson, we put on our security auditor hats and learned to spot several common and dangerous vulnerabilities: improper authority checks, type confusion, and integer overflows. We saw that on Solana, where the client controls everything, you must trust nothing and validate everything.

Today, we shift from identifying problems to proactively preventing them. This is where the Anchor framework truly shines. Instead of writing verbose, manual if checks in every instruction, you can declare your security requirements directly within your Accounts structs. This lesson will teach you how to leverage Anchor's powerful account constraints to build a strong defensive wall around your program logic, focusing specifically on mitigating the re-initialization and type confusion attacks we discussed previously.

The Power of Declarative Security

In the previous lesson, we saw mitigations that looked like this:

// Manual signer check
if !admin.is_signer {
    return Err(ProgramError::MissingRequiredSignature);
}

// Manual ownership check
if config.owner != program_id {
    return Err(ProgramError::IllegalOwner);
}

// Manual type check
if user.discriminant != AccountDiscriminant::Admin {
    return Err(ProgramError::InvalidAccountData);
}

This approach works, but it's repetitive, error-prone, and clutters your business logic. Anchor's philosophy is to handle these boilerplate security checks before your instruction handler is even called. You declare the rules, and Anchor enforces them.

1. Mitigating Re-initialization Attacks

A re-initialization attack occurs when a malicious user calls an initialize function on an account that is already in use. This can wipe the existing data and reset the account to its initial state, potentially leading to theft of funds or a denial of service.

Anchor's primary defense against this is the init constraint.

Program security in anchor framework, Solana smart contract ...

Let's read a concise explanation of this attack and Anchor's approach to fixing it.

Please read the section '4) Reinitialization Attacks'. Focus on understanding: How the init constraint works to prevent re-initialization. The cautionary note about init_if_needed and why it demands extra care.

As the resource explains, the init constraint does two crucial things:

  1. It makes a Cross-Program Invocation (CPI) to the Solana System Program to create a new account. The System Program will fail if an account with the same address already exists (i.e., has a non-zero lamport balance). This is the first line of defense.
  2. After successfully creating the account, Anchor writes an 8-byte discriminator as the first 8 bytes of the new account's data.

This combination makes re-initialization impossible. Any subsequent attempt to call an instruction with init on the same account address will fail.

The init_if_needed constraint is a special tool for making instructions idempotent (callable multiple times without changing the result beyond the initial execution). However, as noted, it's dangerous because it will not prevent your instruction handler from running on an already-initialized account. If you use it, you must add manual checks inside your handler to prevent state from being reset. For most cases, you should stick with init.

2. Preventing Type Confusion (Type Cosplay)

In our last lesson, we discussed the "type cosplay" vulnerability, where a program is tricked into using an account of one type as if it were another.

Account Confusion Illustration
As we saw previously, this diagram illustrates how a program expecting one type of account can be fed another, leading to a 'not matched' error and potential exploits.

Anchor's solution to this is simple, automatic, and highly effective: the discriminator. As mentioned above, when an account is initialized with init, Anchor writes a unique 8-byte identifier derived from the account's struct name (e.g., from MyAccount).

Then, whenever you use the Account<'info, MyAccount> type in an Accounts struct, Anchor automatically performs a check: it reads the first 8 bytes of the account's data and verifies that they match the MyAccount discriminator. If they don't, the transaction is rejected before your code runs. This single feature, used implicitly every time you define an account, completely eliminates the standard type confusion attack vector.

A Hitchhiker's Guide to Solana Program Security

The Helius security guide provides an excellent summary of how Anchor handles this automatically.

Read the section 'Type Cosplay'. Pay close attention to the 'Recommended Mitigation' section and the final paragraph that explains how Anchor's Account<'info, T> wrapper automates this protection.

3. A Practical Example: The has_one and close Constraints

Let's look at how these constraints appear in practice and introduce a few more that are essential for robust security.

Anchor Account Constraints for Solana Programs
This code shows Anchor `Accounts` structs for a counter program. Notice the `init` constraint in the `Create` struct and the `mut` and `has_one` constraints in the `Increment` struct.

The image above shows two instruction contexts, Create and Increment.

  • The Create struct uses init to securely initialize a new Counter account.
  • The Increment struct uses mut to mark the counter as mutable, but more interestingly, it uses has_one = authority.

The has_one = <account_name> constraint is a powerful tool for enforcing ownership or relationships. In this case, has_one = authority is shorthand for constraint = counter.authority == authority.key(). It automatically verifies that the public key stored in the counter account's authority field matches the key of the authority account passed into the instruction, which must also be a Signer. This elegantly solves the "Account Data Matching" vulnerability.

Let's explore this and other powerful constraints.

A Hitchhiker's Guide to Solana Program Security

Let's dive back into the Helius guide to see how has_one and other constraints can solve more common security issues declaratively.

Please read the following three sections: Account Data Matching: Understand how has_one and constraint prevent an attacker from substituting related accounts. Closing Accounts: See why improperly closing accounts is dangerous and how the #[account(close = destination)] constraint provides a secure, all-in-one solution. Duplicate Mutable Accounts: Learn how passing the same account twice can be an attack and how a simple constraint can prevent it.

To summarize the key security constraints you've now seen:

ConstraintPurposeVulnerability Mitigated
initCreates an account and writes its discriminator.Re-initialization Attack
Account<'info, T>Automatically checks the account discriminator.Type Confusion (Cosplay) Attack
has_one = <account>Checks that a Pubkey field in one account matches the key of another.Improper Authority / Account Data Mismatch
close = <destination>Securely closes an account by reclaiming lamports, zeroing data, and setting a closed discriminator.Account Revival / Re-initialization Attack
constraint = <expr>Enforces an arbitrary boolean expression.Custom logic errors (e.g., Duplicate Mutable Accounts)
Signer<'info>Verifies account.is_signer == true.Missing Signer Check
owner = <program_id>Verifies account.owner == <program_id>.Missing Ownership Check

By composing these constraints, you can build a formidable, declarative security model for your program.

Test your understanding!

You are building a game where a central GameConfig account, controlled by an admin, holds the rules. One rule is that the admin can reset a player's PlayerStats account.

Here are the account structs:

#[account]
pub struct GameConfig {
    pub admin: Pubkey,
    pub game_is_active: bool,
}

#[account]
pub struct PlayerStats {
    pub player: Pubkey,
    pub score: u64,
    pub level: u8,
}

Your task is to write the Accounts struct for a reset_player instruction. It must enforce these rules using only constraints:

  1. The PlayerStats account being reset must be mutable.
  2. The GameConfig account's admin field must match the public key of the authority calling the instruction.
  3. The authority must have signed the transaction.
  4. The transaction should only be allowed if game_is_active is true in the GameConfig.
Show answer

Here is the solution for the ResetPlayer accounts struct:

#[derive(Accounts)]
pub struct ResetPlayer<'info> {
    #[account(mut)]
    pub player_stats: Account<'info, PlayerStats>,

    #[account(
        has_one = admin,
        constraint = game_config.game_is_active == true @ CustomError::GameNotActive
    )]
    pub game_config: Account<'info, GameConfig>,

    // The `has_one = admin` on game_config checks that game_config.admin == admin.key().
    // The `Signer` type ensures that the admin account has signed the transaction.
    #[account(mut)] // admin may be the payer, so often marked as mut
    pub admin: Signer<'info>,
}

#[error_code]
pub enum CustomError {
    #[msg("The game is not currently active.")]
    GameNotActive,
}

This struct effectively uses mut, has_one, Signer, and a constraint to enforce all the required security rules declaratively. The custom error on the constraint provides clearer feedback to the user if the check fails.

Conclusion

In this lesson, we transitioned from identifying security flaws to proactively preventing them using Anchor's rich set of account constraints. This declarative approach is a cornerstone of secure and maintainable Solana development.

Key Takeaways:

  • Anchor constraints enforce security rules at the program's entrypoint, before your main logic runs.
  • The init constraint, combined with the automatic discriminator check of Account<'info, T>, provides a powerful two-pronged defense against re-initialization and type confusion attacks.
  • Constraints like has_one, close, and the general-purpose constraint allow you to declaratively enforce complex relationships and state requirements, reducing boilerplate and human error.
  • Thinking in terms of constraints forces you to define the security invariants of your program explicitly, leading to more robust designs.

Preview of the Next Lesson

We've now covered how to build secure programs. But a secure program that is too slow or expensive to run is not practical. In our next lesson, we will shift our focus to performance by learning how to measure the compute unit consumption of a program's instructions. This will allow us to identify bottlenecks and begin the process of optimization, ensuring our applications are not just secure, but also efficient.

Can't find a good explanation? Sign up and we'll make it for you

Sign up