Vulnerability Essay: Discussing PDA and Anchor Account Verification through the Jet Protocol Arbitrary Withdrawal Vulnerability (Released in 2022)

SlowMist
6 min readMay 9, 2023

According to the official blog of Jet Protocol, they recently fixed a bounty vulnerability that could allow malicious users to withdraw funds from any user’s deposit. The SlowMist Security Team conducted a brief analysis of the vulnerability and shared the results as follows.

https://www.jetprotocol.io/posts/jet-bug-disclosure

Related Information

Jet Protocol is a lending market operating on Solana where users can deposit tokens such as USDC and SOL and earn annualized returns. They can also lend out another type of token in a certain proportion. During this process, the contract will give the user a note certificate, which serves as the user’s future withdrawal credentials, known as LP in familiar terms. The reason for this vulnerability is related to the design of this LP.

Compared to Ethereum contracts, Solana contracts do not have the concept of state. Instead, they use an account mechanism, and contract data is stored in related accounts. This mechanism greatly improves the performance of Solana’s blockchain, but it also brings some difficulties to contract development. The biggest difficulty is the need for comprehensive validation of input accounts.

Jet Protocol was developed using the Anchor framework, which was developed by the well-known Serum team on Solana and can streamline many account validation and cross-contract invocation logic.

So how does Anchor work? We can start with a piece of code from Jet Protocol:

  • programs/jet/src/instructions/init_deposit_account.rs

The deposit_account account here is the account used to store LP token data. When a user first uses it, they need to call the contract to generate the account and pay a certain storage fee.

The #[account] macro definition here restricts the account generation rules:

Rule 1: #[account(init, payer = <target_account>, space = <num_bytes>)]

In this constraint, init refers to creating and initializing the account by calling the system contract across contracts, and payer=depositor means that the depositor pays for the storage space fee of the new account.

Rule 2: #[account(seeds = <seeds>, bump)]

This constraint checks whether the given account is a PDA derived by the current executing program. A PDA (Program Derived Address) account is a program-derived account without a private key. Seeds and bump are the generation seeds. If bump is not provided, the Anchor framework defaults to using canonical bump, which can be understood as automatically assigning a deterministic value.

Using a PDA, the program can sign some addresses programmatically without a private key. At the same time, the PDA ensures that no external users can also generate valid signatures for the same address. These addresses are the basis for cross-program invocation, allowing Solana applications to be combined with each other. Here, the “deposits” character + reserve account public key + depositor account public key are used as seeds, and the bump is passed in when the user calls.

Rule 3: #[account(token::mint = <target_account>, token::authority = <target_account>)]

This is an SPL constraint used to verify SPL accounts more conveniently. Here, the deposit_account account is specified as a token account, and its mint permission is deposit_note_mint account, and the authority permission is market_authority.

There are many other macro definitions for Account, which are not mentioned here. For details, please refer to the documentation:

With this background knowledge, we can now directly look at the vulnerability code:

  • programs/jet/src/instructions/withdraw_tokens.rs

Under normal circumstances, when a user calls the withdraw_tokens function to withdraw funds, they will pass in their own LP account, and the contract will destroy their LP and return the corresponding amount of tokens. However, here we can see that the deposit_note_account account is not constrained in any way, and users can freely pass in other users’ LP accounts. Do users not need their signature authorization to use someone else’s LP account?

Through the analysis of the macro definition code earlier, we already know that the market_authority account has the operation permission for LP tokens and does not require the user’s own signature. So what kind of account is market_authority? We can see it here:

  • programs/jet/src/instructions/init_market.rs

This market_authority is also a PDA account. This means that the contract can destroy the user’s LP tokens through its own call. For malicious users, launching an attack is very simple — they only need to set the deposit_note_account account to the target account they want to steal from, and set the withdraw_account account to their own receiving account. Then, they can destroy the user’s LP and withdraw their deposit principal to their own account.

Finally, let’s take a look at the official fix method:

The patch did not directly constrain the deposit_note_account account, but instead removed the PDA signature from the burn operation and changed the authority permission to depositor. This means that users will not be able to directly call this function for withdrawal, but will need to indirectly call it through another function, withdraw(). In the withdraw() function, the account macro definition has been rigorously validated, and if a malicious user passes in someone else’s LP account, they will not pass the macro rules validation, as the depositor needs to satisfy the signer signature check and cannot impersonate someone else’s account.

  • programs/jet/src/instructions/withdraw.rs

Summary

The discovery process of this vulnerability was quite dramatic. The discoverer of the vulnerability, @charlieyouai, shared the process of discovering the vulnerability on his personal Twitter. He found that the burn permission was market_authority, and users could not sign it. He thought it was a bug that would cause the call to fail and users could not withdraw. So, he submitted a bounty vulnerability to the official team and then went to eat, sleep and play.

Later, the official developers realized the seriousness of the issue. Strictly speaking, they knew that this code did not have a withdrawal vulnerability, but rather that anyone could withdraw. Hey, bro, do you know what a well-functioning bug means?! Fortunately, no attack incidents occurred.

Currently, many hacker attacks on Solana are related to account verification issues. The SlowMist Security Team reminds Solana developers to pay attention to conducting rigorous reviews of the account system.

About SlowMist

SlowMist is a blockchain security firm established in January 2018. The firm was started by a team with over ten years of network security experience to become a global force. Our goal is to make the blockchain ecosystem as secure as possible for everyone. We are now a renowned international blockchain security firm that has worked on various well-known projects such as Huobi, OKX, Binance, imToken, Crypto.com, Amber Group, Klaytn, EOS, 1inch, PancakeSwap, TUSD, Alpaca Finance, MultiChain, O3Swap, etc.

Website:
https://www.slowmist.com
Twitter:
https://twitter.com/SlowMist_Team
Github:
https://github.com/slowmist/

--

--

SlowMist

SlowMist is a Blockchain security firm established in 2018, providing services such as security audits, security consultants, red teaming, and more.