Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · March 2026

JupUSD Updates

for Jupiter

Jupiter engaged Guardian to review the security of its JupUSD Updates. From the 02/23/2026 to the 02/26/2026, a team of 2 auditors reviewed the source code in scope.

Published
Rounds
Main Review, Remediation Review
Language
Rust
Chains
Solana
Sector
Stablecoins
  • 0 Critical
  • 0 High
  • 0 Medium
  • 0 Low
  • 11 Informational

4 resolved · 5 acknowledged · 2 pending

Scope

Overview

Jupiter engaged Guardian to review the security of its JupUSD Updates. From the 02/23/2026 to the 02/26/2026, a team of 2 auditors reviewed the source code in scope.

Findings 11

Main Review

10 findings
  1. I-01 Informational Admin Can Close Operator Accounts For Rent Access Control Acknowledged
    Location
    operator.rs: 56-73
    Round
    Main Review

    Description

    delete_operator() only requires the caller’s operator to have OperatorRole::Adminand prevents self-deletion, but does not restrict which deleted_operator can be closed. As a result, any admin can close any other Operator account by passing it as deleted_operator, and can select any payer to receive the reclaimed rent via close = payer.

    Recommendation

    If intended, document this behavior in DeleteOperator/delete_operator() so it’s an explicit design choice. Otherwise, constrain deleted_operator to the caller’s operator_authority (or a designated governance authority) and/or hardcode the close destination to a treasury account instead of a caller-supplied payer.

    Resolution

    Jupiter: Acknowledged.

  2. I-02 Informational Lagging Oracle Lowers Redemption Price Informational Pending
    Location
    Jup-stable
    Round
    Main Review

    Description

    when a vault has >= 2 oracles, parse_oracles always selects the MIN price and redeem uses that MIN for both the max_oracle_price_usd check and payout math, so a lagging/low feed ( spread allowed up to 200bps ) can make redeems pay 1:1 (minus fee only) even when another oracle shows > $1, meaning the higher oracle price would be more profitable to the protocol while redeeming

    Recommendation

    consider setting fees higher than 10 bps to cover the worst case divergence, If this is intended, we are sending this one as information

    If you are planning to support a high volatility stable token as collateral, use the max oracle in redeem and keep using the min oracle in minting

    Resolution

    Jupiter: Pending.

  3. I-03 Informational Typo In Slot_treshold Naming Typo Resolved
    Location
    oracle.rs: 53
    Round
    Main Review

    Description

    The slot_tresholdvariable in from_switchboard_on_demand() spells threshold incorrectly.

    Recommendation

    Fix the typo

    - let slot_treshold = stalesness_threshold * 1000 / clock::DEFAULT_MS_PER_SLOT;
    + let slot_threshold = stalesness_threshold * 1000 / clock::DEFAULT_MS_PER_SLOT;
    

    Resolution

    Jupiter: Resolved.

  4. I-04 Informational Doves Staleness Check Is Stricter Validation Resolved
    Location
    oracle.rs: 97
    Round
    Main Review

    Description

    from_doves() enforces price.timestamp + s > clock.unix_timestamp, which rejects prices exactly at the staleness boundary. The from_pyth_v2() path uses get_price_no_older_than() (>=), and the Switchboard path checks last_update_timestamp + staleness_threshold >= clock.unix_timestamp. This creates inconsistent staleness semantics across oracles.

    Recommendation

    Align the boundary check by using >= in from_doves() or document the stricter Doves behavior for operators.

    Resolution

    Jupiter: Resolved.

  5. I-05 Informational Overriding Fee Race Conditions Warning Acknowledged
    Location
    benefactor.rs
    Round
    Main Review

    Description

    Each benefactor stores an array of 4 fee overrides that can be modified or deleted by an operator with the BenefactorManager role. To do that, the operator has to provide the index of the fee override they wish to modify.

    This creates the possibility of race conditions where the data at that index changed before that and the end state ends up being different than what's expected.

    For example, if slot 0 is empty and two different operators decide to fill it - one for USDC and the other for USDT. After their transactions execute, only one of the overrides will remain, depending on which transaction executed first.

    Recommendation

    Be aware of this behavior and monitor the fee overrides to ensure the state is being updated as intended.

    Resolution

    Jupiter: Acknowledged.

  6. I-06 Informational Slippage Protection On Redeem May Not Be Exact Best Practices Acknowledged
    Location
    user.rs: 299-312
    Round
    Main Review

    Description

    The redeem_amount is compared against min_amount_out to ensure the user is protected against unexpected redeem results.

    require!(
        redeem_amount >= min_amount_out,
        JupStableError::SlippageToleranceExceeded
    );
    

    Then the code enforces that the amount that leaves vault_token_account is the exact redeem_amount

    let amount_before = ctx.accounts.vault_token_account.amount;
    transfer_checked(
        ctx.accounts
            .withdraw_collateral()
            .with_signer(&[authority_seeds!(config.authority_bump)]),
        redeem_amount,
        ctx.accounts.vault_mint.decimals,
    )?;
    ctx.accounts.vault_token_account.reload()?;
    let amount_after = ctx.accounts.vault_token_account.amount;
    require!(
        amount_after == amount_before - redeem_amount,
        JupStableError::InsufficientAmount
    );
    

    However, this doesn't guarantee the user received the same amount. For example, if the underlying token has a fee feature that has been turned on after the mint path was used, the user may receive an amount under their specified slippage parameter.

    Recommendation

    Consider using the delta in the slippage check as well.

    Resolution

    Jupiter: Acknowledged.

  7. I-07 Informational Restart Slot Check May Allow Old Data Warning Acknowledged
    Location
    I-07-3138bda5828c819c9e42eb6987ce5 cd5?pvs=21
    Round
    Main Review

    Description

    The last_update_slot() check only requires that one submission happened after last_restart_slot. However, get_value() still computes the median over all submissions within slot_treshold slots, which can include pre‑restart submissions if the restart slot falls inside the staleness window. This means the post‑restart gate does not fully prevent using pre‑restart data.

    Recommendation

    Document this if it's an accepted behavior.

    Resolution

    Jupiter: Acknowledged.

  8. I-08 Informational Positive Expo Handled As Absolute Scale Informational Resolved
    Location
    oracle.rs
    Round
    Main Review

    Description

    from_doves() converts the price using Decimal::from_i128_with_scale(price.price, price.expo.abs()). This implicitly assumes expo is non‑positive. If expo is ever positive, using abs() would divide by 10^expo instead of multiplying, producing an incorrect price.

    Recommendation

    Pyth usually uses negative expos, so keep this in mind if somehow one of the configured oracles may return positive one.

    Resolution

    Jupiter: Resolved.

  9. I-09 Informational Single Stalesness_threshold Is Reused Informational Acknowledged
    Location
    oracles.rs
    Round
    Main Review

    Description

    parse_oracles() forwards the same stalesness_threshold to from_pyth_v2(), from_switchboard_on_demand(), and from_doves(). If any of the oracles update times differs, stale price may be used.

    Recommendation

    Ensure the staleness used will be suitable for all of the configured oracles.

    Resolution

    Jupiter: Acknowledged.

  10. I-10 Informational Confidence Semantics Differ Across Oracles Math Resolved
    Location
    oracles.rs
    Round
    Main Review

    Description

    from_pyth_v2() compares Pyth price.conf (an explicit confidence interval half-width) against MAX_CONFIDENCE_BPS, while from_switchboard_on_demand() compares std_dev() (standard deviation of submissions) against the same threshold. and because of that the end result is not exactly the same. The std must be in MAX_CONFIDENCE_BPS of the actual price.

    Recommendation

    If the current behavior is intended, document the assumption in the oracle handling. Otherwise implement different check for the std.

    Resolution

    Jupiter: Resolved.

Remediation Review

1 finding
  1. I-01 Informational Manipulating Oracle Can Affect Redeems Informational Pending
    Location
    oracle.rs
    Round
    Remediation Review

    Description

    The final decision for pricing redeems is to use the minimum price returned by all the configured oracles in order to minimize spread. This increases the potential impact of oracle manipulation, as now only one of the oracles has to return a lower price in order to affect redeems.

    For example, if the real price is 1.01, a manipulated oracle may return 0.999 and trigger redeem with the 1 branch. It's expected that the fee charged will compensate for this, but it still depends on the exact configuration.

    Recommendation

    Be aware of this and keep monitoring the oracles after deployment.

    Resolution

    Jupiter: Pending.

More from Jupiter

  1. Yield Store

    47 findings1 critical · 6 high 47 findings: 1 critical, 6 high, 16 medium, 12 low, 12 informational
  2. Offerbook

    36 findings 36 findings: 4 medium, 15 low, 17 informational
  3. JupUSD Stablecoin

    18 findings 18 findings: 9 low, 9 informational

Put your code through the same review.

This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.

Get a quote