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
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-
I-01 Informational Admin Can Close Operator Accounts For Rent Access Control Acknowledged
Description
delete_operator()only requires the caller’soperatorto haveOperatorRole::Adminand prevents self-deletion, but does not restrict whichdeleted_operatorcan be closed. As a result, any admin can close any otherOperatoraccount by passing it asdeleted_operator, and can select anypayerto receive the reclaimed rent viaclose = payer.Recommendation
If intended, document this behavior in
DeleteOperator/delete_operator()so it’s an explicit design choice. Otherwise, constraindeleted_operatorto the caller’soperator_authority(or a designated governance authority) and/or hardcode the close destination to a treasury account instead of a caller-suppliedpayer.Resolution
Jupiter: Acknowledged.
-
I-02 Informational Lagging Oracle Lowers Redemption Price Informational Pending
Description
when a vault has >= 2 oracles,
parse_oraclesalways selects the MIN price and redeem uses that MIN for both themax_oracle_price_usdcheck 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 redeemingRecommendation
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.
-
I-03 Informational Typo In Slot_treshold Naming Typo Resolved
Description
The
slot_tresholdvariableinfrom_switchboard_on_demand()spellsthresholdincorrectly.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.
-
I-04 Informational Doves Staleness Check Is Stricter Validation Resolved
Description
from_doves()enforces price.timestamp + s > clock.unix_timestamp, which rejects prices exactly at the staleness boundary. Thefrom_pyth_v2()path usesget_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
>=infrom_doves()or document the stricter Doves behavior for operators.Resolution
Jupiter: Resolved.
-
I-05 Informational Overriding Fee Race Conditions Warning Acknowledged
Description
Each benefactor stores an array of 4 fee overrides that can be modified or deleted by an operator with the
BenefactorManagerrole. 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.
-
I-06 Informational Slippage Protection On Redeem May Not Be Exact Best Practices Acknowledged
Description
The
redeem_amountis compared againstmin_amount_outto 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_accountis the exactredeem_amountlet 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.
-
I-07 Informational Restart Slot Check May Allow Old Data Warning Acknowledged
Description
The
last_update_slot()check only requires that one submission happened afterlast_restart_slot. However,get_value()still computes the median over all submissions withinslot_tresholdslots, 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.
-
I-08 Informational Positive Expo Handled As Absolute Scale Informational Resolved
Description
from_doves()converts the price usingDecimal::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.
-
I-09 Informational Single Stalesness_threshold Is Reused Informational Acknowledged
Description
parse_oracles()forwards the samestalesness_thresholdtofrom_pyth_v2(),from_switchboard_on_demand(), andfrom_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.
-
I-10 Informational Confidence Semantics Differ Across Oracles Math Resolved
Description
from_pyth_v2()compares Pythprice.conf(an explicit confidence interval half-width) againstMAX_CONFIDENCE_BPS, whilefrom_switchboard_on_demand()comparesstd_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 inMAX_CONFIDENCE_BPSof 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-
I-01 Informational Manipulating Oracle Can Affect Redeems Informational Pending
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 return0.999and trigger redeem with the1branch. 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.
No findings match.
More from Jupiter
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.
