Beosin Discovers Critical IDL Instruction Vulnerability in Older Versions of the Solana Anchor Framework

iconMetaEra
Share
AI summary iconSummary
Beosin’s security team has identified a critical IDL instruction vulnerability in older versions of the Solana Anchor framework. The flaw enables attackers to hijack program-owned PDA accounts by exploiting AccountInfo declarations, allowing fund theft in two steps without requiring user privileges. This incident underscores the urgent need for a more robust compliance framework in smart contract development. With MiCA (EU Markets in Crypto-Assets Regulation) approaching, such vulnerabilities highlight the critical importance of real-time security audits and strict adherence to regulatory standards.

Anchor is the most mainstream development framework in the Solana ecosystem. It significantly lowers the barrier to entry through features such as declarative account validation, automatic serialization, and built-in security checks. However, while providing convenience, the framework silently injects internal instructions into every program that developers may not be aware of. These “hidden” instructions can be exploited by attackers under specific conditions, leading to significant financial losses.

In this article, the Beosin security team will reveal a critical vulnerability pattern: in older versions of Anchor, when developers declare program-owned PDA accounts using AccountInfo, attackers can seize control of the account and drain all SOL from it in just two steps by exploiting the IDL instruction automatically injected by Anchor, without requiring any privileged access.

I. Analysis of IDL Instructions and Related Mechanisms

1.1 IDL Instruction

Anchor automatically injects a set of IDL (Interface Definition Language) management instructions into every program unless the no-idl feature is explicitly enabled during build. These instructions include:

IdlCreateAccount: Create an on-chain IDL account

IdlWrite: Write data to an IDL account / buffer account

IdlSetAuthority: Change the authority of the IDL account

IdlCloseAccount: Close the IDL account and transfer all lamports to the specified recipient.

IdlResizeAccount: Resize the IDL account

IdlCreateBuffer: Create an IDL buffer account (IDL Buffer)

IdlSetBuffer: Overwrite the official IDL account with data from the buffer account

These instructions were originally designed for on-chain IDL management, but they grant special operational capabilities over accounts owned by the program (reading and writing data, changing authorities, closing accounts, and transferring lamports)—this is precisely the core of the vulnerability. Attackers do not need to invoke any of the developer-written business instructions; they can directly call these built-in instructions.

1.2 IDL Buffer Account

The IDL buffer account is a temporary account introduced by Anchor to upload large IDL data in segments. Since the complete IDL (after JSON compression) may exceed the size limit of a single transaction, Anchor allows you to first create a buffer using IdlCreateBuffer, then write the data in batches using multiple IdlWrite instructions, and finally submit it all at once using IdlSetBuffer.

The key point is the data structure of the IDL account / buffer account: it begins with a fixed-layout header containing an authority: Pubkey field (referred to as the controller in this test output). IDL instructions use this field to determine “who has authority to operate on this account.”

The problem is that the IdlCreateBuffer logic in older versions of Anchor treats any account owned by the program—as long as it is passed in—as a buffer account and directly sets the authority to the transaction signer. This means that if an account’s owner is the program (e.g., a program-derived address vault), an attacker can designate themselves as its controller and then use IdlCloseAccount to legitimately transfer all SOL from the account.

1.3 Conditions for Triggering the Vulnerability

Triggering this vulnerability requires meeting the following conditions simultaneously:

  • Using an older version of Anchor: Dangerous IDL instructions lack proper account validation, allowing business accounts to be mistakenly treated as IDL accounts.
  • no-idl is not enabled: The program retains the default injected IDL instruction entry during build, exposing the attack surface.
  • Declare the PDA owned by the program using AccountInfo: Developers use raw AccountInfo to carry funding accounts (such as treasury PDAs), but without the discriminator/owner validation provided by Anchor-typed accounts (such as Account), this account appears indistinguishable from IDL accounts to IDL instructions.
  • The account is owned by and holds lamports: owner == this program is a prerequisite for the IDL instruction to operate on it; holding SOL gives it value to be emptied.

After meeting the above conditions, the attacker only needs two ordinary transactions: first use IdlCreateBuffer to seize control of the controller, then use IdlCloseAccount to transfer all SOL, completing the attack without requiring any permissions from the target program.

1.4 Attack Chain

Below, based on Anchor's internal implementation, we'll break down step by step how an attacker can use only two built-in instructions to disguise a regular vault as an IDL account and empty it.

Overview of the attack process

Step 1: IdlCreateBuffer(treasury, signer = attacker)    
└─ treasury.controller ==> attacker (becomes controller)  
Step 2: IdlCloseAccount(treasury, authority = attacker, dest = attacker)  
└─ treasury.lamports ==> attacker (drains the treasury)

Step 1: IdlCreateBuffer takes authority

The internal implementation of Anchor is approximately as follows:

#[derive(Accounts)] pub struct IdlCreateBuffer {  
#[account(zero)]          // ← Key: an account with its discriminator all 0  pub buffer: Account,  
pub authority: Signer,}    
pub fn idl_create_buffer(ctx: Context) -> Result 
{ 
 let idl = &mut ctx.accounts.buffer;  idl.authority = *ctx.accounts.authority.key; // set authority as authority  Ok(())}

The meaning of #[account(zero)] is: accept an account whose discriminator is entirely zero and is owned by the program as an uninitialized IDL account to be initialized. The vault恰好 satisfies both of these conditions:

The status of the vault

Conditions

owned by program

After init_if_needed, the owner is set to this program.

discriminator is all zeros

Using the AccountInfo type, Anchor does not write the discriminator, and the data is all zeros.

The attacker then passes the vault into IdlCreateBuffer:

  • Anchor writes the discriminator of the IdlAccount into the first 8 bytes before the vault;
  • Write the attacker's public key into the authority field.

At this point, the vault has been "spoofed" into appearing as an IdlAccount with the attacker as its authority—while the SOL inside remains untouched.

Step 2: IdlCloseAccount —— Clear Funds

#[derive(Accounts)]pub struct IdlCloseAccount {  #[account(mut, has_one = authority)]  // ← check authority == signer  pub account: Account,  pub authority: Signer,  #[account(mut)]  pub destination: AccountInfo,  // ← attack wallet}

The vault has now been fully verified:

  • discriminator matches IdlAccount
  • The authority field = attacker's public key
  • has_one = authority verification passed

Thus, all lamports (the entire amount of SOL deposited by the user) within the vault were legally transferred to the attacker’s account, leaving the vault with zero balance.

Root cause:

The fatal root of this vulnerability is the use of AccountInfo in deposit.rs instead of typed Anchor Accounts:

(1) Account types (such as Account) write an 8-byte discriminator specific to that structure during initialization;

(2) Once its own discriminator is written, Anchor can no longer treat it as an IdlAccount (discriminator mismatch), causing the first step, IdlCreateBuffer, to fail and breaking the attack chain.

II. Case Study

The following test is based on a PoC cross-chain bridge contract built with Anchor 0.31.0. The test simulates a real bridge treasury whose owner is the bridge program itself, containing 1.001281 SOL deposited by users. The attacker's wallet initially holds 2 SOL and has no privileges.

Project

Value / Description

Bridge Program

CJutNlr8d3oxdb11ReOLaZd5jPsqUxgwt2mv5e7equtE

Treasury account

DxGkzaMhP5Wy824GhoehErdD6MudfuEPGPwaV2s77FsM

Vault Owner

= Bridge program (program-owned PDA)

Vault initial balance

1.001281 SOL (user deposit)

Initial Controller

0x0000...0000 (all zeros, not set)

Initial balance of attacker's wallet

2.000000 SOL

Required privileges

NONE (No authorization required)

Number of transactions

2 transactions

Final vault balance

0.000000 SOL (emptied)

Attackers profit

+1.001281 SOL

PoC screenshot (tests/poc-idl-hijack.ts):

https://wdcdn.qpic.cn/MTMxMDI3MDE1MTgxMDU0NzA_812435_a37-yrU_dY02i1_I_1785900257?w=1080&h=1287

As shown, Step 1’s IdlCreateBuffer sets the attacker as the vault’s controller (with no change in balance); Step 2’s IdlCloseAccount transfers the entire 1.001281 SOL from the vault to the attacker’s wallet, reducing the vault’s balance to zero.

Repair/Protection Recommendations:

(1) Upgrade Anchor version: The latest version has fixed this issue (IDL instructions strictly distinguish between IDL accounts and business accounts); avoiding the use of older Anchor versions is the most direct and fundamental defense measure.

(2) Enable no-idl during build: Explicitly disable IDL instruction injection in production applications to eliminate this attack surface at its source.

(3) Use typed accounts instead of raw AccountInfo: Use typed accounts such as Account or SystemAccount with discriminator and owner validation to hold funding accounts, preventing them from being misidentified by IDL instructions.

(4) Minimize the number of withdrawable accounts owned by the program: Apply explicit owner/seeds/discriminator constraints to PDAs holding funds, and validate account discriminators in critical instructions.

Conclusion

The vulnerability fundamentally stems from the combination of “framework-hidden instructions” and “missing account type validation.” Developers should recognize that the IDL instructions injected by Anchor by default represent real attack surfaces; upgrading the framework version and applying strong type constraints to funding accounts can effectively eliminate the risk of unauthorized fund draining.

Beosin is a leading blockchain security and regulatory compliance technology company specializing in pre-launch smart contract audits, real-time security risk monitoring and blocking, asset recovery, virtual asset anti-money laundering (AML), and investigative tracking. Beosin has provided “one-stop” blockchain compliance products and security services to regulatory and law enforcement agencies in over 20 countries and regions, more than 200 virtual asset service providers, and 4,500+ Web3 projects. Please leave us a message in our official account to get in touch.

Disclaimer: The information on this page may have been obtained from third parties and does not necessarily reflect the views or opinions of KuCoin. This content is provided for general informational purposes only, without any representation or warranty of any kind, nor shall it be construed as financial or investment advice. KuCoin shall not be liable for any errors or omissions, or for any outcomes resulting from the use of this information. Investments in digital assets can be risky. Please carefully evaluate the risks of a product and your risk tolerance based on your own financial circumstances. For more information, please refer to our Terms of Use and Risk Disclosure.