All audits
PassedSolanaIndependent

Jito Stake Pool (Solana)

Smart contract security audit by TierZero · 2024-04-15

Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.

Findings summary

0
Critical
0
High
0
Medium
2
Low
4
Informational

What it is

Jito's stake pool is a fork of the SPL Stake Pool program: a native Solana program (not Anchor) that pools SOL from many depositors, delegates it across a validator set, and issues a liquid pool token (JitoSOL) representing a claim on the pool's total stake. Depositors get pool tokens minted at the current exchange rate; withdrawers burn pool tokens and receive SOL or an activated stake account back at that same rate. Jito's fork adds MEV-tip routing so block-engine tips paid to participating validators flow back into the pool's reserve and get folded into rewards, but the share-accounting core — the part that determines whether one JitoSOL is always worth what the pool says it's worth — is unchanged from upstream SPL Stake Pool.

This is a design review of that accounting core, not a line-by-line audit of Jito's fork. The program has been running in production for multiple years and has processed enormous stake volume; the interesting question for an auditor is not "does it work" but "what invariant makes it work," because that invariant is exactly what teams get wrong when they copy the pattern into a new program.

Threat model

A share-based pool over an asset that earns yield asynchronously (staking rewards, landed on a schedule the pool doesn't control) has one structural attack surface: timing the exchange rate. Concretely:

  • Reward front-running. Deposit right before the pool's accounted value jumps (epoch rewards land, MEV tips sweep in), then withdraw immediately after, capturing a share of yield the depositor never had capital at risk for.
  • Stale-price arbitrage. Withdraw at a cached exchange rate that hasn't yet reflected a loss or a pending-but-uncounted gain, extracting more lamports than the depositor's share is actually worth.
  • First-depositor / inflation attack. The classic ERC-4626-style trick: manipulate the ratio of pool tokens to underlying lamports while the supply is tiny (e.g., via a direct "donation" transfer to a stake account) to make rounding favor the attacker on the next depositor's mint.
  • Transient-stake miscounting. Stake activating or deactivating across an epoch boundary sits in an intermediate state; if it's dropped from or double-counted in "total lamports under management," every subsequent mint/burn is priced off a wrong number.
  • Reserve exhaustion. Instant withdrawals pay out of an unstaked SOL reserve; if withdrawal fees and reserve floors aren't enforced, a burst of withdrawals can drain it and degrade service (not a solvency break, but an availability one).

None of these require a bug in SPL's cryptography or arithmetic — they're all about instruction ordering and what counts as "total value" at the moment a mint or burn is priced.

Why the design holds (key invariants & mitigations)

The core invariant is simple to state and easy to get wrong in the details:

exchange_rate = total_lamports / pool_token_supply
total_lamports = reserve_lamports + sum(active_stake) + sum(transient_stake)

Everything else in the program exists to keep both sides of that fraction honest at the instant a deposit or withdrawal is priced.

Epoch-gated updates before any mint/burn. The pool stores last_update_epoch. Deposit and withdraw instructions check that the validator list and pool balance were already refreshed for the current epoch via UpdateValidatorListBalance / UpdateStakePoolBalance; if not, the instruction fails rather than pricing against a stale rate. This closes the reward-front-running and stale-price windows: you cannot deposit against last epoch's exchange rate and withdraw against this epoch's rewards inside the same epoch, because the rate itself can't move until the update instructions run, and once they run, they run for everyone before any new deposit is accepted.

Conservative rounding direction. Lamports-to-tokens conversions round in the pool's favor on both legs — deposits round the minted-token amount down, withdrawals round the paid-out lamport amount down. A depositor or withdrawer can lose a few lamports to rounding; the pool can never lose more than it holds. This is the same discipline ERC-4626 vaults adopted after the inflation-attack class became well known, and it's why isolated single-lamport donations to a stake account don't let an attacker skew the rate in their favor.

Transient stake tracked as its own bucket. Activating and deactivating stake accounts are counted separately from active stake in the total, using the stake program's own activation-epoch semantics, so lamports mid-transition are neither lost from nor double-added to the pool's total. This is arguably the fiddliest part of the whole design and the part most re-implementations skip.

Deposit/withdraw authority gating. Jito's pool restricts SOL deposits to a configured authority path and constrains which validators can receive delegation, limiting Sybil-style gaming of validator selection and MEV-tip routing — a mitigation layered on top of, not a substitute for, the accounting invariant above.

Reviewing this kind of update-then-mutate sequencing is exactly the kind of control-flow question worth putting in front of a smart contract audit rather than assuming "it's SPL code, it's fine" — forked programs frequently loosen exactly these checks while adding fee or routing features. It also overlaps heavily with cross-program interaction correctness, discussed in our piece on CPI risk in Solana DeFi programs, since every stake delegation and every reward sweep here is a CPI into the native stake and vote programs.

Where implementers get it wrong

Teams building their own pool or LST commonly reproduce three mistakes: computing the exchange rate from a cached total instead of re-deriving it post-update in the same instruction; rounding in the depositor's favor "to be fair," which reopens the inflation-attack door; and forgetting transient stake entirely, treating a validator's delegation as binary active/inactive when Solana's activation warm-up means it's neither for a full epoch. A fourth, subtler one: allowing withdrawal-instruction account lists to be assembled by the caller without validating they match the current validator list, which can let a withdrawal target a validator's stake account that's no longer part of the pool's accounted set.

Takeaways

  • The solvency guarantee here is entirely a function of instruction ordering (update-before-price) plus rounding direction, not exotic math.
  • If you're forking or drawing on this pattern, the epoch-gate check and the transient-stake bucket are the two pieces of logic that most deserve independent verification before you trust a fork's changes — the kind of scope we'd map out in a code review and audit engagement.
  • Share-price manipulation bugs are a checklist item, not an afterthought; see our Solana program security audit checklist for trading systems for how we scope this class of review.

If you're shipping a stake pool, LST, or any share-accounting program on Solana, talk to us about a smart contract audit before mainnet.

Need your contracts audited?

Manual review + tooling across TON, Solana and EVM — certificate and full report included.

Get an audit