Medium severity
Deprecated Pyth getPrice: a staleness window your contract did not choose
Updated 2026-10-27
Pyth’s Solidity SDK used to give IPyth three convenience reads: getPrice, getEmaPrice and getValidTimePeriod. Pyth’s API reference marks all three as deprecated and recommends getPriceNoOlderThan and getEmaPriceNoOlderThan, which let the caller set the maximum age. Version 4.0.0 of @pythnetwork/pyth-sdk-solidity (August 2024) dropped all three from the interface, and the SDK’s AbstractPyth base keeps them “only for backward compatibility”. They are not unchecked reads: getPrice(id) is implemented as getPriceNoOlderThan(id, getValidTimePeriod()) and reverts with StalePrice once the stored price is older than that window. The problem is whose window it is. getValidTimePeriod() returns one number per Pyth deployment, set by Pyth and changeable through its governance, not by your contract. When we read it on 2026-10-27 it was 60 seconds on Base, Arbitrum One and OP Mainnet, and 120 seconds on Ethereum. A contract that prices collateral through getPrice has handed one of its risk parameters to someone else. This is the third Pyth integration check, next to the publish time and the confidence interval.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
library PythStructs {
struct Price {
int64 price;
uint64 conf;
int32 expo;
uint256 publishTime;
}
}
// Excerpt of IPyth as it shipped before SDK 4.0.0
interface IPyth {
function getValidTimePeriod() external view returns (uint256);
function getPrice(bytes32 id)
external view returns (PythStructs.Price memory);
function getEmaPrice(bytes32 id)
external view returns (PythStructs.Price memory);
}
contract Lending {
IPyth public immutable pyth;
bytes32 public immutable ethUsd;
constructor(IPyth _pyth, bytes32 _ethUsd) {
pyth = _pyth;
ethUsd = _ethUsd;
}
// BUG: getPrice is deprecated. It returns any price younger than
// getValidTimePeriod(), a window Pyth sets per chain, so how stale
// a price this market accepts is decided outside the contract and
// changes with the chain it is deployed on.
function collateralValue(uint256 ethAmount) public view returns (uint256) {
PythStructs.Price memory p = pyth.getPrice(ethUsd);
return ethAmount * uint256(uint64(p.price)); // feed units: scale by 10^expo
}
}Deploy this Lending contract unchanged on Base and on Ethereum. On Base, a price that has not been refreshed for more than a minute makes collateralValue revert; on Ethereum the same code keeps valuing collateral at a quote up to two minutes old. Neither bound came out of the protocol’s risk analysis: a liquidation engine on a volatile asset may want ten or twenty seconds, while a vault holding a slow-moving asset may accept more. Because the window lives in Pyth’s contract, it can also move under you: a governance change on that chain changes how stale a price your market accepts, with no upgrade, review or event of your own. In a fast market that gap is the exploit window, since on Ethereum a borrower can draw against collateral marked at a price up to two minutes old.
There is a maintenance trap as well. Against the 4.x SDK, pyth.getPrice(id) no longer compiles (solc 0.8.19 reports Member "getPrice" not found or not visible after argument-dependent lookup in contract IPyth.), and the one-word change that turns the build green again, getPriceUnsafe, removes the age check altogether, which is the bug on the unchecked publish time page. Code that borrows the window explicitly has the same problem in another shape:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
library PythStructs {
struct Price {
int64 price;
uint64 conf;
int32 expo;
uint256 publishTime;
}
}
interface IPyth {
function getValidTimePeriod() external view returns (uint256);
function getPriceUnsafe(bytes32 id)
external view returns (PythStructs.Price memory);
}
contract Perp {
IPyth public immutable pyth;
constructor(IPyth _pyth) { pyth = _pyth; }
// BUG: the freshness check is real, but its bound is still Pyth's
// deprecated chain-wide window, not one this market chose.
function markPrice(bytes32 id) public view returns (int64) {
PythStructs.Price memory p = pyth.getPriceUnsafe(id);
require(p.publishTime + pyth.getValidTimePeriod() >= block.timestamp, "stale");
require(p.price > 0, "bad price");
require(p.conf * 100 <= uint64(p.price), "price too uncertain");
return p.price;
}
}The fix
Name the maximum age in the call. getPriceNoOlderThan(id, age) returns the price only if it was published within age seconds and reverts with StalePrice otherwise, so the bound is visible in your code, reviewed with it, and the same on every chain. The fixed contract below picks 30 seconds for its market and adds the positive-price and confidence-band checks from the companion pages; it clears all three Pyth detectors.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
library PythStructs {
struct Price {
int64 price;
uint64 conf;
int32 expo;
uint256 publishTime;
}
}
interface IPyth {
function getPriceNoOlderThan(bytes32 id, uint256 age)
external view returns (PythStructs.Price memory);
}
contract Lending {
IPyth public immutable pyth;
bytes32 public immutable ethUsd;
uint256 public constant MAX_AGE = 30; // seconds; this market's choice
constructor(IPyth _pyth, bytes32 _ethUsd) {
pyth = _pyth;
ethUsd = _ethUsd;
}
// FIX: name the maximum age in the call. getPriceNoOlderThan reverts
// with StalePrice once the stored price is older than MAX_AGE, on
// every chain, whatever Pyth's own default window is.
function collateralValue(uint256 ethAmount) public view returns (uint256) {
PythStructs.Price memory p = pyth.getPriceNoOlderThan(ethUsd, MAX_AGE);
require(p.price > 0, "bad price");
require(p.conf * 100 <= uint64(p.price), "price too uncertain");
return ethAmount * uint256(uint64(p.price)); // feed units: scale by 10^expo
}
}If the right age really differs by chain or asset, make it your own parameter, set per market in the constructor or by your own governance with sane limits, rather than inheriting Pyth’s default. A tight bound has an operational cost on a pull oracle: reads revert until someone pushes a fresh update. Pyth’s own SDK example pays getUpdateFee, calls updatePriceFeeds with fresh update data in the same transaction, and then reads with a 10-second bound. The moving-average reads follow the same rule: replace getEmaPrice with getEmaPriceNoOlderThan.
A note on scope
Slither’s pyth-deprecated-functions detector reports every high-level call named getPrice, getEmaPrice or getValidTimePeriod on a type named IPyth, including calls inside inherited functions. It is a name match, so it is exact about what it sees and blind to the rest. In our tests it did not flag the same getPrice call made through a differently named interface (IPythOracle), through a contract type instead of the interface (such as the SDK’s AbstractPyth), or as a low-level staticcall built from the selector. It also ignores what you do with the result: a getPrice call followed by your own publishTime check is still reported. With a tighter check of your own the behaviour is sound for as long as Pyth’s window stays above your bound, and the finding is a migration item (the code stops compiling on a 4.x SDK); with a looser one, Pyth’s window still wins and reads revert sooner than you intended. A clean result means no deprecated call through IPyth, not that the oracle handling is sound.
Scan your contract for this
pactlint flags pyth-deprecated-functions, the two other Pyth checks 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.
A call to a deprecated Pyth read is reported by Slither’s stock
pyth-deprecated-functions detector (“Pyth deprecated functions”), rated medium impact at high confidence,
which pactlint surfaces at medium severity with no refinement of its own. We confirmed this on solc 0.8.19 with a full pactlint
scan: the Lending contract above gets three medium findings on the pyth.getPrice(ethUsd) line, this one
plus pyth-unchecked-publishtime and pyth-unchecked-confidence; the Perp contract gets this
finding on its getValidTimePeriod() call; the fixed version has no findings. In a Foundry test against the
MockPyth contract from SDK 4.3.1, the deprecated version accepted a 45-second-old price under a 60-second window that
the fixed one rejected with StalePrice, and the same deprecated code rejected a 90-second-old price under a
60-second window but accepted it under a 120-second one. The per-chain windows are getValidTimePeriod() reads of the Pyth Core contracts listed in
Pyth’s documentation, made on 2026-10-27. 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.