On a $20M Bug in Jet Protocol

April 1, 2022

Recently, Charlie You disclosed a vulnerability in the Jet Protocol (see tweet). The vulnerability would have caused a $20m loss of Jet users’ funds if exploited. Fortunately, Jet patched it before any user was affected.

The Soteria team identified something tricky in Jet-v1’s code and had a discussion with Charlie shortly after the disclosure. It turns out that the vulnerability has a different cause (unexpected by Charlie). Here is a summary:

  • The cause is actually an unvalidated input account, and it would have been detected by Soteria Auto Auditor easily.
  • The vulnerability was introduced in commit 6133de on Dec 15, 2021, but not exploitable.
  • The vulnerability became exploitable because of a follow-up commit 1f7da4 on Dec 21, 2021.
  • The exploitable window spanned over five weeks from Dec 21 to Jan 27.
  • Anchor works in the correct way as we expect.

Details

1. The real vulnerability

In commit 6133de, Jet added a file withdraw_tokens.rs, which defines a function handler :

The function does two things:

1. Transfer tokens from the reserve to the user’s withdraw_account

2. Burn notes from the user’s provided deposit_note_account

The function note_burn_context creates a Burn instruction on the deposit_note_account (line 80) with market_authority (line 82):

The real vulnerability lies in that deposit_note_account is an unvalidated input account (line 54):

The market_authority is the authority of all users’ deposit_account (code):

Therefore, an attack may supply any user’s deposit_note_account and burn notes successfully.

2. The vulnerability was not (yet) exploitable

However, the vulnerability is not exploitable because the withdraw_tokens::handler function in withdraw_tokens.rs is not an attack surface: it cannot be directly called by users.

It can only be called from the handler function of withdraw.rs (line 74):

The withdraw::handler function is an attack surface (via the Withdraw instruction):

However, the deposit_account in the Withdraw instruction is properly validated:

In the above, depositor is signer, and deposit_account is unique to the depositor (depositor.key is part of the PDA seeds).

deposit_account is passed as deposit_note_account to withdraw_tokens::handler.

Therefore, attackers cannot exploit the vulnerability because deposit_account is validated.

3. When the vulnerability became exploitable

In commit 1f7da4, Jet introduced the WithdrawTokens instruction, which sets the withdraw_tokens::handler function as an attack surface:

Thereafter, the vulnerability became directly exploitable.

4. When and how the vulnerability was fixed

On Jan 27, the vulnerability was fixed in commit-f4bb3b by changing the authority to depositor (line 82):

The depositor is signer:

This ensures that an attacker cannot create a fake depositor to invoke WithdrawTokens successfully.

In fact, the only way to call withdraw_tokens::handler successfully is through the Withdraw instruction. The withdraw::handler function creates a CPI to call the WithdrawTokens instruction (line 108):

The CPI passes market_authority as the depositor (line 89), which will be used as the authority for the Burn instruction:

5. Anchor works in the correct way as we expect

Initially, we suspected that the vulnerability might be due to additional attack surfaces generated by Anchor accidentally. For instance, whether Anchor-generated attack surfaces not defined in the #program macro, but imported by instructions::* or use super::* :

However, after close inspection, we confirmed that Anchor works as what we expected: its generated code only contains surfaces defined in the #program macro.

How Soteria detects the vulnerability

Soteria Auto Auditor can detect this vulnerability automatically (i.e., deposit_note_account is an unvalidated input account). The following shows a screenshot of the report (line 54):

The unvalidated input account vulnerability introduced in commit 3178f93.

Inspired by Charlie’s original tweet, Soteria team also added a new vulnerability signature to the SVE database:

  • SVE1032: IncorrectAuthorityAccount — The PDA operation may be incorrectly used as shared authority and may allow any account to transfer or burn tokens.

The algorithm is actually simple: (1) if any PDA account (e.g., market_authority) is used as authority to transfer/mint/burn tokens, and (2) if the from/to account in the transfer/mint/burn instruction is not connected with a signer account (e.g., PDA seeds).

If both conditions are true, a potential vulnerability will be flagged.

The following shows a screenshot of the IncorrectAuthorityAccount report:

In conclusion, (1) Soteria’s tool would’ve detected this vulnerability, (2) Soteria learned a new vulnerability signature from this incident, and added to the tool.


Soteria Audit

Soteria is founded by leading minds in the fields of blockchain security and software verification.

We are pleased to provide full audit services to high-impact Dapps on Solana. Please visit soteria.dev or email contact@soteria.dev