diff --git a/src/generated/FlareFtsoWords.pointers.sol b/src/generated/FlareFtsoWords.pointers.sol index 5fe54d7..0768631 100644 --- a/src/generated/FlareFtsoWords.pointers.sol +++ b/src/generated/FlareFtsoWords.pointers.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0x96e4ec5ff213f69e32f76c5ce42fb5ae8a42af3ad080341ce1d1b74a0061723d); +bytes32 constant BYTECODE_HASH = bytes32(0x1009eb4c3a377a298162ec21d6f2d479377f243349cca5eea4314474a788eef9); /// @dev The hash of the meta that describes the contract. bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0x8717d07737e3cedcdddea6cd3337ae762d7089918bf8d818fb0afc5b63e3985a); diff --git a/src/lib/price/LibFtsoCurrentPriceUsd.sol b/src/lib/price/LibFtsoCurrentPriceUsd.sol index 7cded63..f9bcaff 100644 --- a/src/lib/price/LibFtsoCurrentPriceUsd.sol +++ b/src/lib/price/LibFtsoCurrentPriceUsd.sol @@ -45,11 +45,17 @@ library LibFtsoCurrentPriceUsd { revert InconsistentFtso(); } - // Handle stale prices. - //slither-disable-next-line timestamp - if (block.timestamp > priceTimestamp + timeout) { - revert StalePrice(priceTimestamp, timeout); + // Handle stale prices. Use unchecked to avoid Panic(0x11) from a + // misbehaving FTSO that reports a near-uint256-max timestamp; an + // overflowing deadline is treated as stale. + //slither-disable-start timestamp + unchecked { + uint256 deadline = priceTimestamp + timeout; + if (deadline < priceTimestamp || block.timestamp > deadline) { + revert StalePrice(priceTimestamp, timeout); + } } + //slither-disable-end return (price, decimals); } diff --git a/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol b/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol index b48f5e2..b4d80b9 100644 --- a/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol +++ b/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol @@ -161,6 +161,41 @@ contract LibOpFtsoCurrentPriceUsdTest is FtsoTest { this.externalRun(operand, inputs); } + /// When priceTimestamp + timeout overflows uint256, the staleness check must + /// revert with StalePrice rather than panic with Panic(0x11). + function testRunStaleOverflow( + OperandV2 operand, + string memory symbol, + uint256 timeout, + uint256 currentTime, + PriceDetails memory priceDetails, + CurrentPrice memory currentPrice + ) external { + vm.assume(bytes(symbol).length <= 31); + uint256 intSymbol = IntOrAString.unwrap(LibIntOrAString.fromStringV3(symbol)); + + timeout = bound(timeout, 1, uint256(int256(type(int224).max))); + // Force priceTimestamp + timeout to overflow. + currentPrice.timestamp = bound(currentPrice.timestamp, type(uint256).max - timeout + 1, type(uint256).max); + currentTime = bound(currentTime, 0, type(uint256).max); + vm.warp(currentTime); + + conformPriceDetails(priceDetails, currentPrice); + finalizePrice(priceDetails); + + mockRegistry(); + mockFtsoRegistry(FTSO, symbol); + activateFtso(); + mockPriceDetails(priceDetails); + mockPrice(FTSO, currentPrice); + + StackItem[] memory inputs = new StackItem[](2); + inputs[0] = StackItem.wrap(bytes32(intSymbol)); + inputs[1] = StackItem.wrap(Float.unwrap(LibDecimalFloat.fromFixedDecimalLosslessPacked(timeout, 0))); + vm.expectRevert(abi.encodeWithSelector(StalePrice.selector, currentPrice.timestamp, timeout)); + this.externalRun(operand, inputs); + } + /// Deterministic boundary: when `block.timestamp == priceTimestamp + timeout` /// the price is NOT yet stale (the staleness check is strictly /// greater-than). It MUST be accepted and returned, not reverted as