Medium severity
Using a Pyth price without checking its confidence interval
Updated 2026-10-22
A Pyth price feed does not hand you a single number. Every getPrice call returns a struct with a price, an exponent, a publish time — and a conf field, the confidence interval: Pyth’s own estimate of how uncertain that price is right now. The band is tight when markets are liquid and orderly, and it widens — sometimes to a large fraction of the price itself — during volatility, thin liquidity, venue outages or a deliberate squeeze. Code that reads price and ignores conf throws that signal away: it treats a price Pyth is flagging as barely trustworthy exactly like a price Pyth is confident in. Pyth’s own best-practices guide is explicit that an integrator should look at the confidence interval and refuse or discount a price whose band is too wide. This is the Pyth counterpart to the staleness-and-bounds checks a Chainlink consumer owes its feed (see unchecked Chainlink price).
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
library PythStructs {
struct Price {
int64 price;
uint64 conf; // confidence interval, same exponent as price
int32 expo;
uint256 publishTime;
}
}
interface IPyth {
function getPriceNoOlderThan(bytes32 id, uint256 age)
external view returns (PythStructs.Price memory);
}
contract Vault {
IPyth public pyth;
constructor(IPyth _pyth) { pyth = _pyth; }
// BUG: reads `price` and ignores `conf`. When Pyth widens the
// confidence band -- exactly when the price is least reliable --
// this values collateral at a number Pyth does not stand behind.
function valueOf(bytes32 id, uint256 amount) external view returns (uint256) {
PythStructs.Price memory p = pyth.getPriceNoOlderThan(id, 60);
return amount * uint256(uint64(p.price));
}
}Picture a lending market that prices collateral through this valueOf. Under normal conditions the confidence band is a basis point or two and nothing looks wrong. Then the asset dislocates: the venues Pyth aggregates disagree, the published conf blows out to several percent of the price, but valueOf still returns the mid-point as if it were exact. A borrower can draw against collateral marked far above what the market will actually bear, or a liquidation can trigger — or fail to trigger — on a number nobody should be acting on, and the protocol eats the gap as bad debt. The confidence interval is the one piece of data that would have told the contract “not now”, and it was never read.
The fix
Read conf and make the price prove itself before you use it: reject (or clamp, or fall back) when the band is wider than a limit you choose. The simplest form is an absolute cap per feed; a production integration usually ties the bound to the price itself — refuse when conf is more than, say, 0.5% of price — and checks staleness as well. The example below applies an absolute cap, which is enough for the detector to report zero findings because the confidence is now part of a real check.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
// ... PythStructs and IPyth as above ...
contract Vault {
IPyth public pyth;
uint64 public maxConf; // widest confidence band this feed may carry
constructor(IPyth _pyth, uint64 _maxConf) {
pyth = _pyth;
maxConf = _maxConf;
}
// FIX: the price must come with a confidence band inside the cap,
// otherwise revert instead of acting on a number Pyth is unsure of.
function valueOf(bytes32 id, uint256 amount) external view returns (uint256) {
PythStructs.Price memory p = pyth.getPriceNoOlderThan(id, 60);
require(p.conf <= maxConf, "price too uncertain");
return amount * uint256(uint64(p.price));
}
}Prefer the ratio form in real code — a fixed absolute cap drifts as the price moves — and combine it with a positive-price check and a freshness bound (getPriceNoOlderThan already rejects a stale price, which is the subject of the separate pyth-unchecked-publishtime detector). The rule that avoids the class is simple: a Pyth price is a price and an uncertainty; if your contract never reads the uncertainty, it is trusting a number the oracle explicitly told it not to trust blindly.
A note on scope
Slither’s pyth-unchecked-confidence detector fires when a value returned by an IPyth price call is used without the conf field ever being read into a comparison on a path after the call. It runs at high confidence because the shape is clear-cut, but it is deliberately shallow in one direction: it counts the confidence as “checked” only when the conf member feeds directly into a comparison. We verified that a ratio check which first casts conf (for example uint256(p.conf) * 10000 <= uint256(uint64(p.price)) * bps) still trips the detector even though it is a stronger check — so a finding on a contract that does validate the band through a cast or a helper is a false positive; read it in context. The reverse limit also holds: the detector confirms only that you read conf in some comparison, not that your bound is sensible, and it keys on the IPyth interface name, so a feed consumed through a differently named wrapper will not be matched. Treat a clean result as “no obvious unchecked Pyth price”, not as proof the oracle handling is sound.
Scan your contract for this
pactlint flags pyth-unchecked-confidence and other recurring DeFi bug classes in Solidity
source or a verified contract on Ethereum or Base. Pay per call in USDC on Base, Polygon, Solana, BNB Chain or Arbitrum with x402,
or on Tempo with MPP, no signup; the first few scans each day are free.
An unchecked Pyth confidence interval is reported by Slither’s stock
pyth-unchecked-confidence detector (“Pyth unchecked confidence level”),
rated medium impact at high confidence, which pactlint surfaces at medium severity — it is not downgraded or denied in policy, no
description rule touches it, and it carries no worker-side triage rule, so it surfaces exactly as mapped. We confirmed this on
solc 0.8.19 with Slither 0.11.6: the Vault contract above fires pyth-unchecked-confidence at medium impact and
high confidence (“Pyth price conf field is not checked in Vault.valueOf(bytes32,uint256)”);
adding the require(p.conf <= maxConf) check clears it to zero findings. Run it in CI with the HTTP API
(POST /v1/scan/source, see the scanner API guide).
Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.