Solana Bug Bounty Hunting With Soteria

October 23, 2021

Recently, using Soteria we identified a vulnerability in an on-chain Solana smart contract (jet-v1) and have been awarded a bug bounty through Immunefi. We thank the Jet Protocol team and Immunefi for their quick responses and generous support. The fix has been applied in this commit.

This article shares our bug hunting experience with Soteria.

What is Soteria?

Soteria is an automated verification tool powered by the GreenCore technology. It scans the contract source code and looks for common pitfalls such as missing ownership check, missing signer check, Solana account confusions, arbitrary signed program invocation, and integer overflow/underflows.

Soteria is currently under active development and checkers for more vulnerabilities are being added. More details and a tutorial could be found in [Soteria — A vulnerability scanner for Solana smart contracts].

Shortlisting potential vulnerabilities using Soteria

Soteria is integrated with the contract build process. Getting a list of potential defects is as simple as one command line.

Suppose we have already obtained a fresh copy of a contract. We just need to invoke soteria . or soteria -analyzeAll . in the Solana program’s directory where the build configuration file Xargo.toml exists.

The analysis is pretty fast and Soteria will report a list of issues, which we may want to take a closer look at. In our scenario, among the four issues reported by Soteria, the following two are particularly interesting.

[Issue-1] Unsafe Arithmetic Operation

Found a potential vulnerability at line 300, column 9 in programs/jet/src/state/obligation.rsThe add operation may result in overflows:

 298|            .count(); 
 299|
>300|        loans + collaterals
 301|    }
 302|

 >>>Stack Trace:>>>jet::dispatch [programs/jet/src/lib.rs:37]
 >>>  jet::__private::__global::init_collateral_account [programs/jet/src/lib.rs:37]
 >>>    jet::jet::init_collateral_account [programs/jet/src/lib.rs:37]
 >>>      jet::instructions::init_collateral_account::handler [programs/jet/src/lib.rs:70]
 >>>        jet::state::obligation::Obligation::register_collateral [programs/jet/src/instructions/init_collateral_account.rs:65]
 >>>          jet::state::obligation::Obligation::position_count2 [programs/jet/src/state/obligation.rs:71]

[Issue-2] Unsafe Arithmetic Operation

Found a potential vulnerability at line 122, column 24 in programs/jet/src/instructions/borrow.rsThe add operation may result in overflows:

 120|    let req_tokens = amount.as_tokens(reserve_info, Rounding::Down);
 121|    let fees = reserve.borrow_fee(req_tokens);
>122|    let token_amount = req_tokens + fees; 
 123|
 124|    // Calculate the number of notes to create to match the value being
 
 >>>Stack Trace:>>>jet::dispatch [programs/jet/src/lib.rs:37]
 >>>  jet::__private::__global::borrow [programs/jet/src/lib.rs:37]
 >>>    jet::jet::borrow [programs/jet/src/lib.rs:37]
 >>>      jet::instructions::borrow::handler [programs/jet/src/lib.rs:128]

In the above, Soteria reports the issue types, issue locations, and the stack traces demonstrating how the issues may happen. The next step is to review the code and figure out if those issues can lead to security exploits.

Manual review

[Issue-1] Unsafe Arithmetic Operation

The types of collaterals and loans are both usize . If there are too many loans in the list, the indicated add operation can overflow. So, this is a true positive in theory. However, the exploit depends on the state instead of user input. Since it's not (directly) user-controllable, it's not an ideal candidate for exploit PoC (Proof of Concept) creation.

[Issue-2] Unsafe Arithmetic Operation

In borrow.rs, the computation of the total token amount Y for a borrow request of amount X can overflow. The possible consequences of this overflow are

  1. A large amount (requested loan amount X) will be subtracted from the total_deposits in reserve, although only a small amount (Y because of the overflow) is eventually loaned to the borrower. The amount of X-Y in total_deposits will be lost.
  2. After a successful loan transaction, a borrower who requests a large amount X may actually receive a small amount loan Y.

In particular:

PoC creation

Finding the appropriate inputs and satisfying all sanity checks in the contract are the main challenges for the PoC creation. In our scenario, we have to prepare appropriate deposit and collateral amounts to make the smart contract happy. After a few attempts, we found the magic numbers, and the following screenshot demonstrates the overflow and the loss of the holdings in the reserve total_deposits.

  • The customer requested to borrow `18,428,315,757,952,000,000`. An additional fee (`0.1%`) of `18,428,315,757,952,000` will be required to process the borrow request.
  • However, the largest number that can be represented by u64 is `18,446,744,073,709,551,615`. So, an overflow will occur in `token_amount = req_tokens + fees`, which gives a relative small number `400,384`.
  • After a successful transaction, `18,428,315,757,952,000,000` was requested but `400,384` was received by the borrower.
  • More importantly, the `total_deposits` was deducted by the amount requested instead of the actual amount borrowed. In other words, after borrowing `400,284`, the total holdings in the reserve decreased from `18,440,000,000,000,000,000` to `11,684,242,048,000,000`, which is inconsistent and a huge loss.

Developer response

We reported our findings together with a PoC to the developers. The developers acknowledged our findings with a reward and commented

“…although this is not exploitable in our program as deployed, and in general requires an improbable concentration of token supply in Jet’s reserves, it is a problem in principle, that we will patch, and review the rest of the codebase for other instances of.

We genuinely appreciate the time spent analyzing our code.”

The issue has been patched shortly after the report.

Takeaways

  1. The initial automated verification by Soteria is highly effective to filter out unimportant code and produce a shortlist of potential issues that require more attention, which saved significant manual audit time in our experience.
  2. Generating penetration tests is challenging as it usually requires some knowledge of the logic in the smart contract. However, extra automation like symbolic execution or fuzzing could be done to reduce manual efforts.

For all blogs by Soteria, Please visit https://www.soteria.dev/blogs