# Jet
While conducting a code review for [Jet Protocol](https://www.jetprotocol.io/), we discovered a critical vulnerability which could lead to loss of funds. By overpaying unhealthy loans, liquidators could theoretically drain collateral from the protocol's vaults. This bug was present in some form since Jet's initial deploy, and we reported it to them in April.
## Background
Jet is a lending protocol built on Solana. Similar to secured loans, people borrow money by pledging some sort of collateral. The on-chain program acts as a mediator between borrowers and lenders, pooling together assets. This allows it to seize collateral when necessary, then liquidate in exchange for the original loaned asset.
Let's unpack what the last sentence means. The central concern of any lending protocol is the fact that people may default on their debt. Jet addresses this by selling the borrower's collateral above market price, which incentivizes third parties, dubbed liquidators, to purchase it. While traditional collateral (e.g. a car or house) is relatively illiquid, in this context "collateral" refers to SPL tokens of another mint. For instance, I may pledge wrapped BTC as collateral in order to borrow wrapped SOL - assuming both currencies are supported by the market. Crucially, liquidators pay with the missing asset (in this example, wrapped SOL) so that Jet may repay the original lenders.
However, this approach has an issue: how can the protocol sell above market price without incurring a net loss? The answer is that a borrower's collateral should always be worth more than their loan. Specifically, the ratio between collateral and loan value must exceed some threshold to be considered healthy. Of course, a borrower's obligation may naturally turn unhealthy due to interest, changing prices, and a variety of other factors. Only then does the program allow liquidators to purchase collateral and stabilize the ratio.
Taking a step back, we see that both borrowers and liquidators are incentivized to repay loaned assets, thanks to the collateral's premium. Although Jet Protocol has additional details like fees and interest rate curves, this simplified model should suffice for understanding the bug.
## Discovery
Let's take a look at how Jet implements repayment and liquidation. At any time, borrowers may use the `repay` instruction to pay their own loan. Notice that they may specify an amount greater than their outstanding debt, in which case the program takes the minimum. This prevents the borrower from accidentally losing assets. ([source code](https://github.com/jet-lab/jet-v1/blob/a53d6eaad0e18044003b927e134351f6746ee0e1/programs/jet/src/instructions/repay.rs#L172-L174))
```rust
// Calculate the number of tokens and notes that match the value being repaid
let payoff_notes = amount.as_loan_notes(reserve_info, Rounding::Down)?;
let payoff_notes = std::cmp::min(payoff_notes, token::accessor::amount(loan_account)?);
```
When an obligation is unhealthy, liquidators may use the `liquidate` instruction to purchase collateral. Liquidation consists of two steps: transferring collateral and repaying the loan. Interestingly, Jet implements the second step by reusing the `repay` instruction's functionality. ([source code](https://github.com/jet-lab/jet-v1/blob/a53d6eaad0e18044003b927e134351f6746ee0e1/programs/jet/src/instructions/liquidate.rs#L113-L115))
```rust
let collateral_amount = transfer_collateral(ctx.accounts, amount, min_collateral)?;
repay(&ctx, amount)?;
```
This type of logic should raise concern. As we have seen in `repay`, `amount` is an arbitrary quantity that does not reflect the **actual** repaid amount. But `transfer_collateral` does not take this into account, assumes the borrower owed that much in loans, and awards collateral accordingly. ([source code](https://github.com/jet-lab/jet-v1/blob/a53d6eaad0e18044003b927e134351f6746ee0e1/programs/jet/src/instructions/liquidate.rs#L149-L162))
```rust
// Calclulate number of tokens being repaid to figure out the value
let repaid_notes_amount = reserve.amount(amount.as_loan_notes(reserve_info, Rounding::Down)?);
// Calculate the appropriate amount of the collateral that the
// liquidator should receive in return for this repayment
let collateral_account = accounts.collateral_account.key();
let loan_account = accounts.loan_account.key();
let collateral_amount = obligation.liquidate(
market_reserves,
clock.slot,
&collateral_account,
&loan_account,
repaid_notes_amount,
)?;
```
This inconsistency has severe consequences. For example, suppose Alice borrows 1000 wETH and 1 wSOL by pledging wBTC as collateral. If the obligation becomes unhealthy (perhaps wBTC loses value), Bob has the opportunity to liquidate. He offers to repay an exorbitant amount of wSOL and receives all of the collateral in return. But in reality, Bob will only pay 1 wSOL! Furthermore, Alice can intentionally take a borderline unhealthy loan only to subsequently liquidate herself. This demonstrates how anyone can actively drain funds from the protocol.
## Closing Thoughts
After reporting the vulnerability to Jet, we decided the best fix was to modify `transfer_collateral` to be consistent with `repay`, i.e. take the minimum between the specified amount and the actual debt. This was merged in [#394](https://github.com/jet-lab/jet-v1/pull/394). We also checked other lending protocols for similar issues.
This bug illustrates the importance of intuitive behavior and documentation. Skimming the code, it felt reasonable to assume that `repay` disallowed overpaying loans. But the function "failed silently," and subsequent logic did not consider this behavior. As auditors and developers, we should always be on the lookout for similarly implicit assumptions.