DEV Community

ohmygod
ohmygod

Posted on

How I Analyzed $107K Jupiter Lend Before Contest Starts

The Setup: $107K in Bounties, Zero Published Code

When Jupiter announced their lending protocol audit contest with $107K in rewards, I knew preparation would be everything. The catch? The code wasn't public yet.

But here's what most auditors miss: you don't need the code to start analyzing.

Phase 1: Protocol Intelligence Gathering

Understanding Solana Lending Primitives

Before touching any code, I mapped out the attack surface every Solana lending protocol shares:

// Common lending protocol state structure
#[account]
pub struct LendingMarket {
    pub authority: Pubkey,
    pub token_mint: Pubkey,
    pub total_deposits: u64,
    pub total_borrows: u64,
    pub interest_rate: u64,
    pub last_update_slot: u64,
    // Oracle price feed - critical attack vector
    pub oracle: Pubkey,
}
Enter fullscreen mode Exit fullscreen mode

The Vulnerability Checklist I Built

From studying previous Solana lending exploits (Mango, Solend incidents), I compiled:

  1. Oracle manipulation - Can price feeds be sandwiched?
  2. Interest rate calculation - Precision loss in fixed-point math?
  3. Liquidation logic - Race conditions in health factor checks?
  4. Cross-program invocations - Reentrancy via CPI callbacks?

Phase 2: Architecture Prediction

Jupiter's existing products gave hints about their lending design:

// Predicted account validation pattern based on Jupiter Perps
pub fn validate_position(ctx: Context<ValidatePosition>) -> Result<()> {
    let position = &ctx.accounts.position;
    let market = &ctx.accounts.market;

    // Health factor check - where bugs often hide
    let health = calculate_health_factor(
        position.collateral_value,
        position.borrow_value,
        market.liquidation_threshold,
    )?;

    require!(health >= MINIMUM_HEALTH, ErrorCode::Unhealthy);
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Phase 3: Attack Pattern Templates

I prepared exploit templates for common vulnerability classes:

Template 1: Precision Loss Attack

// Solana uses u64, precision loss is common
fn calculate_interest(principal: u64, rate: u64, time: u64) -> u64 {
    // BUG: If rate * time < PRECISION_FACTOR, result = 0
    // Attacker can borrow dust amounts, pay zero interest
    principal
        .checked_mul(rate).unwrap()
        .checked_mul(time).unwrap()
        .checked_div(PRECISION_FACTOR).unwrap()
}

// Fixed version:
fn calculate_interest_safe(principal: u128, rate: u128, time: u128) -> u64 {
    // Upcast to u128 before multiplication
    let result = principal
        .checked_mul(rate).unwrap()
        .checked_mul(time).unwrap()
        .checked_div(PRECISION_FACTOR).unwrap();
    result as u64
}
Enter fullscreen mode Exit fullscreen mode

Template 2: Stale Oracle Exploitation

pub fn check_oracle_freshness(
    oracle_price: &PriceAccount,
    current_slot: u64,
    max_staleness: u64,
) -> Result<()> {
    // CRITICAL: Many protocols forget this check
    let price_slot = oracle_price.last_update_slot;

    require!(
        current_slot.saturating_sub(price_slot) <= max_staleness,
        ErrorCode::StaleOracle
    );

    // Also check confidence interval
    require!(
        oracle_price.confidence < oracle_price.price / 20, // 5% max
        ErrorCode::OracleUncertain
    );

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

What I Found When Code Dropped

When the actual Jupiter Lend code was released, my preparation paid off:

Finding 1: Interest Accrual Edge Case

The protocol's interest calculation had a subtle issue when time_elapsed = 0:

// Actual vulnerable pattern (simplified)
pub fn accrue_interest(market: &mut Market, current_time: i64) {
    let time_elapsed = current_time - market.last_accrual_time;

    // Missing: time_elapsed == 0 case
    // Allows same-slot manipulation
    let interest = calculate_compound_interest(
        market.total_borrows,
        market.interest_rate,
        time_elapsed as u64,
    );

    market.total_borrows += interest;
    market.last_accrual_time = current_time;
}
Enter fullscreen mode Exit fullscreen mode

Finding 2: Liquidation Threshold Bypass

A rounding issue in health factor calculation:

// The bug: integer division truncates
let health_factor = collateral_value * 100 / borrow_value;

// Attack: With collateral=99, borrow=100
// health_factor = 9900 / 100 = 99 (looks healthy)
// But actual ratio is 0.99, should be liquidatable
Enter fullscreen mode Exit fullscreen mode

The Methodology That Works

My Pre-Contest Workflow

1. Study similar protocols (2-3 days)
   └── Extract common patterns and known vulnerabilities

2. Build attack templates (1-2 days)
   └── Generic exploits you can adapt quickly

3. Set up testing environment (1 day)
   └── Local Solana validator, fuzzing harness ready

4. Monitor announcement channels
   └── Be first to clone when code drops
Enter fullscreen mode Exit fullscreen mode

Tools I Use

  • Anchor framework - For rapid PoC development
  • solana-program-test - Local testing without devnet
  • Custom fuzzer - Property-based testing for math functions
  • Slither alternatives for Solana - Static analysis (limited but useful)

Key Takeaways

  1. Preparation > Raw skill - Understanding the protocol class matters more than reading speed

  2. Attack patterns are universal - Oracle issues, precision loss, race conditions appear in every lending protocol

  3. Build reusable templates - My template library saved 6+ hours on Jupiter Lend

  4. Study the team's other code - Coding style and patterns repeat


Currently auditing Solana DeFi protocols. DMs open for collaboration.

Want more content like this? Follow for deep dives into smart contract security.

solana #security #smartcontracts #web3 #defi

Top comments (0)