How I Analyzed $107K Jupiter Lend Before Contest Starts
Source: Dev.to
The Setup: $107K in Bounties, Zero Published Code
When Jupiter announced their lending protocol audit contest with $107 K in rewards, I knew preparation would be everything. The catch? The code wasn’t public yet.
But you don’t need the code to start analyzing.
Attack‑surface mapping
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,
}
From studying previous Solana lending exploits (Mango, Solend incidents), I compiled the following classes of vulnerabilities:
- Oracle manipulation – Can price feeds be sandwiched?
- Interest‑rate calculation – Precision loss in fixed‑point math?
- Liquidation logic – Race conditions in health‑factor checks?
- Cross‑program invocations – Re‑entrancy via CPI callbacks?
Jupiter’s existing products gave hints about their lending design:
// Predicted account validation pattern based on Jupiter Perps
pub fn validate_position(ctx: Context) -> 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(())
}
Exploit templates for common vulnerability classes
// Solana uses u64, precision loss is common
fn calculate_interest(principal: u64, rate: u64, time: u64) -> u64 {
// BUG: If rate * time overflows 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
}
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,
// (truncated in original)
);
Ok(())
}
- Attack patterns are universal – Oracle issues, precision loss, and race conditions appear in every lending protocol.
- Reusable templates save time – My template library saved 6+ hours on Jupiter Lend.
- Team’s coding style repeats – Studying other projects from the same team helps anticipate design choices.