← back

Solana: The State of TOCTOU Attacks

I recently read Blockaid's 2024 article on Time-of-Check to Time-of-Use (TOCTOU) attacks in the Solana ecosystem. I was curious whether this type of attack is still a threat in 2026.

I decided the best way for me to do so was to deploy a smart contract which uses this draining mechanism and test it on major wallets.

This article summarizes my findings from observing how different wallets handle this attack, potential mitigation, and a small piece on how major drainers utilize this technique.

How the Attack Works

When users sign transactions on Solana, wallets usually perform a local simulation against the current blockchain state. The simulation result is then displayed to the user in the form of estimated balance changes, such as:

"+1 SOL" "-50 USDC" "Simulation failed; this transaction will likely fail"

These simulations are important because they help users understand what will happen after signing a transaction.

However, these simulations are based on a snapshot of state at simulation time.

TOCTOU attacks take advantage of the trust that users have in their wallet's simulation. They benefit from the fact that the simulated state (S₀) differs from the state at execution time (S₁).

S₀ (simulation state): used to predict transaction outcome S₁ (execution state): actual state where the transaction is processed

If S₀ ≠ S₁, then the simulation may not accurately reflect what happens in S₁.

For my smart contract, I decided to use a PDA/Vault account-based approach.

Here is a simple chart I made of the flow of this attack:

A vault PDA account would be filled with Solana (or tokens). The simulation would show the user "+1 SOL", so the vault will need to have >= 1 SOL.

For each "victim", a PDA account would be derived using their pubkey as the seed. Note that this PDA account would initially be empty.

The dApp would request signTransaction (NOT signAndSendTransaction).

The contract would then check: is the derived PDA account empty? If so, then send the user 1 SOL.

Since as of now the PDA account is empty, the simulation will show the user "+1 SOL". Seeing this, the user signs, and the signed blob is sent to the dApp's backend (not yet submitted to the blockchain).

Here, Jito's transaction bundling feature is utilized.

Using Jito, you can bundle multiple transactions together which get executed atomically.

The backend builds a bundled transaction consisting of the following:

Tx1: Sending a small amount of SOL to the derived PDA account Tx2: The transaction the victim signed Tx3: Close the derived PDA account, reclaiming SOL to the attacker's wallet Tx4: Jito tip

These transactions will execute exactly in this order, and if any one of these fails, the entire bundle is dropped. This is to the attacker's advantage, as it eliminates any chance of the victim actually receiving 1 SOL from the vault.

The backend submits this bundle to the blockchain, and the user's wallet is emptied. A completely different outcome than what the simulation showed.

Mitigation

Does this mean that simulations themselves are a security risk to users? No. We just need a way to enforce the effects of the transaction on-chain = the effects of the transaction during simulation.

A solution is Lighthouse.

Lighthouse (by Jac0xb) is a protocol designed specifically to mitigate TOCTOU attacks by making sure that the on-chain outcome of a transaction matches the outcome simulated by the wallet.

Lighthouse embeds assertion instructions at the end of a transaction, checking conditions like:

If at the time where the transaction is submitted, these assertions fail (i.e., different from when the transaction was simulated), the whole transaction is dropped and not executed.

In our case, if Lighthouse is implemented on the victim's wallet, it will add an assertion like "SOL balance >= initial - fees - 1" (simplification).

The user will still see "+1 SOL" during simulation. But, once the transaction is submitted, Lighthouse will check at the end if the user's final balance does indeed increase by 1. Since it won't, the assertion will fail, the transaction will be reverted, and the drain will be prevented.

Are Major Wallets Vulnerable? | Testing as of May 2026

So, even though we have an effective solution to TOCTOU attacks, why are they still prevalent? The answer is because barely any major wallets have adopted Lighthouse.

Here is how testing went:

Phantom

After clicking the button to initiate the transaction, Phantom showed an estimated balance change of +0.1 SOL.

After clicking confirm, the Jito bundle wasn't submitted due to bundle simulation failure.

Here are the specific logs:

The final instruction was injected by Phantom. Program "L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95" corresponds to the Lighthouse Protocol program.

This means that Phantom integrates Lighthouse (via "Guard Instructions"), which prevents this drain. In fact, Phantom was the first major wallet to implement it, and they announced it in this article.

Solflare

Similarly, Solflare shows an estimated change of +0.1 SOL.

The transaction also fails due to Lighthouse's assertion check(s):

This means that Solflare also implements the Lighthouse Protocol.

Backpack

Backpack was different from Phantom and Solflare:

Backpack showed that we would receive 0.1 SOL.

But, after clicking approve, the wallet was drained almost instantly. In a real scenario, a user thinks they'll get some SOL, but they end up losing everything because they trusted what the simulation showed them.

This means that Backpack is vulnerable to this attack.

MetaMask

This was interesting. The simulation showed the expected +0.1 SOL, but the actual transaction failed with ComputationalBudgetExceeded.

At first I suspected MetaMask had implemented Lighthouse, but I deduced that MetaMask was just estimating CU usage during simulation and injecting a compute budget with a buffer. This is probably done to lower gas fees rather than as a security measure. Bypassing it was easy: I artificially inflated the innocent branch's CU usage to exceed that of the malicious branch with a useless loop:

for _ in 0..120 { let _ = Pubkey::find_program_address(&[b"vault"], ctx.program_id); }

After that, the malicious branch executed successfully:

MetaMask is vulnerable.

Jupiter

Jupiter is also vulnerable:

Exodus

Exodus is also vulnerable.

Out of the 6 wallets I tested, only 2 have adopted Lighthouse. While two of the most popular Solana wallets integrate the protocol, it still means that users who decide to use any other wallets are at risk.

Conclusion

To remain a leader in Web3, it’s important that the Solana ecosystem addresses issues like this, especially when an open-source solution already exists. It’s fair to argue that users should just be more vigilant, and that simulations shouldn't be seen as absolute, but if Lighthouse could be the only thing standing between someone and losing their life savings, I don’t think adoption should be seen as optional.

Note: This article is for research/educational purposes only. Prior to publishing, I have responsibly disclosed/reported this issue (via bug bounty programs) to vulnerable wallets mentioned in this article and beyond to the best of my ability. Most wallets did not respond or marked this vulnerability as informative.

Extra: How the Infamous Angel Drainer Operates

I purposely got myself drained by a malicious dApp which uses Angel Drainer to analyze on-chain how the drain works.

Due to the highly obfuscated nature of Angel, these are purely (somewhat confident) assumptions and nothing should be taken definitively.

Angel utilizes simulation spoofing, which means they likely use something like Jito to make S₁ differ from S₀, as we have been discussing.

Their actual drain strategy differs from the one my contract used though, and they utilize Solana's nonce feature.

Angel converts the victim's account into a nonce account controlled by the attacker. Here's how:

Since the nonce authority was given to the attacker, they are the only one who can control the account once it's converted. The victim's account is essentially bricked.

This is why Angel and similar Solana drainers are so effective, as they weaponize a legitimate Solana feature against users.

Fortunately, this can also be mitigated by implementing Lighthouse. Lighthouse has an accountInfoAssertion field and a dedicated DataLength field within that. Since regular accounts have a data size of 0 bytes, and nonce accounts have a data size of 80 bytes, you can simply just compare if S₀'s DataLength = S₁'s DataLength.