From 206fdd36cecfcbef6e0002bfe1ba690813e8a6b1 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 16 Jun 2026 18:52:22 +0000 Subject: [PATCH 1/5] Fix staleness overflow: use subtraction instead of addition in stale check priceTimestamp + timeout can overflow uint256 when timeout is very large, causing Panic(0x11) instead of a clean StalePrice revert. Use subtraction (block.timestamp - priceTimestamp > timeout) which cannot overflow because block.timestamp >= priceTimestamp for any valid timestamp from the FTSO. Also adds a regression test exercising timeout = type(uint256).max so that restoring the addition-based check causes the test to fail with Panic(0x11). Closes #102 Closes #105 Co-Authored-By: Claude --- src/lib/lts/LibFtsoV2LTS.sol | 4 +-- src/lib/price/LibFtsoCurrentPriceUsd.sol | 5 ++-- .../src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol | 28 +++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/lib/lts/LibFtsoV2LTS.sol b/src/lib/lts/LibFtsoV2LTS.sol index d406dd5..0dda6d0 100644 --- a/src/lib/lts/LibFtsoV2LTS.sol +++ b/src/lib/lts/LibFtsoV2LTS.sol @@ -95,9 +95,9 @@ library LibFtsoV2LTS { (uint256 value, uint64 timestamp) = ftsoRegistry.getFeedByIdInWei{value: fee}(feedId); - // Handle stale prices. + // Handle stale prices. Subtraction avoids overflow when timeout is large. //slither-disable-next-line timestamp - if (block.timestamp > timestamp + timeout) { + if (block.timestamp > timestamp && block.timestamp - timestamp > timeout) { revert StalePrice(timestamp, timeout); } diff --git a/src/lib/price/LibFtsoCurrentPriceUsd.sol b/src/lib/price/LibFtsoCurrentPriceUsd.sol index 7cded63..ed877e8 100644 --- a/src/lib/price/LibFtsoCurrentPriceUsd.sol +++ b/src/lib/price/LibFtsoCurrentPriceUsd.sol @@ -45,9 +45,10 @@ library LibFtsoCurrentPriceUsd { revert InconsistentFtso(); } - // Handle stale prices. + // Handle stale prices. Subtraction avoids a uint256 overflow when timeout + // is very large; such a timeout is effectively infinite (never stale). //slither-disable-next-line timestamp - if (block.timestamp > priceTimestamp + timeout) { + if (block.timestamp > priceTimestamp && block.timestamp - priceTimestamp > timeout) { revert StalePrice(priceTimestamp, timeout); } diff --git a/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol b/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol index 730693e..3c8f1ad 100644 --- a/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol +++ b/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol @@ -10,6 +10,7 @@ import {LibFork} from "test/fork/LibFork.sol"; import {BLOCK_NUMBER} from "../registry/LibFlareContractRegistry.t.sol"; import {InactiveFtso, PriceNotFinalized, StalePrice, DecimalsTooLarge} from "src/err/ErrFtso.sol"; import {LibDecimalFloat, Float} from "rain-math-float-0.1.1/src/lib/LibDecimalFloat.sol"; +import {LibFtsoCurrentPriceUsd} from "src/lib/price/LibFtsoCurrentPriceUsd.sol"; contract LibOpFtsoCurrentPriceUsdTest is FtsoTest { function externalRun(OperandV2 operand, StackItem[] memory inputs) @@ -194,6 +195,33 @@ contract LibOpFtsoCurrentPriceUsdTest is FtsoTest { this.externalRun(operand, inputs); } + /// A timeout so large that priceTimestamp + timeout overflows uint256 must not + /// panic. The staleness check uses subtraction instead of addition, so an + /// overflowing timeout is treated as infinite (price is never stale). + /// Mutation: restoring the addition-based check causes Panic(0x11) here. + function testRunTimestampPlusTimeoutOverflow(string memory symbol, PriceDetails memory priceDetails) external { + vm.assume(bytes(symbol).length > 0 && bytes(symbol).length <= 31); + + CurrentPrice memory currentPrice; + currentPrice.price = 1; + currentPrice.timestamp = 1; + currentPrice.decimals = 5; + + vm.warp(type(uint256).max); + + conformPriceDetails(priceDetails, currentPrice); + finalizePrice(priceDetails); + + mockRegistry(); + mockFtsoRegistry(FTSO, symbol); + activateFtso(); + mockPriceDetails(priceDetails); + mockPrice(FTSO, currentPrice); + + // timeout = max → priceTimestamp + timeout overflows; must not panic. + LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd(symbol, type(uint256).max); + } + /// An inactive FTSO should revert. function testRunFtsoNotActive(OperandV2 operand, string memory symbol, uint256 timeout) external { vm.assume(bytes(symbol).length < 0x20); From 24408fba91a772040455fa3857c65c47f0363dba Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 16 Jun 2026 22:37:45 +0000 Subject: [PATCH 2/5] build: regenerate pointers/meta after bytecode change Co-Authored-By: Claude Opus 4.8 --- src/generated/FlareFtsoWords.pointers.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generated/FlareFtsoWords.pointers.sol b/src/generated/FlareFtsoWords.pointers.sol index 5fe54d7..9695c1b 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(0x84d8e6cee8d70e1f48eac054d2f6bd2bf85f17a5b3c18017176f73d6bcbfba2a); /// @dev The hash of the meta that describes the contract. bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0x8717d07737e3cedcdddea6cd3337ae762d7089918bf8d818fb0afc5b63e3985a); From 4993100e5c23ab49f0e7c13e4c3e33fe91d8c8ca Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 19 Jun 2026 17:41:42 +0000 Subject: [PATCH 3/5] merge(main): resolve conflicts [merge-update] From 119f4c0ce67b5a21e4177b6222fcee154e3c9e4b Mon Sep 17 00:00:00 2001 From: David Meister Date: Wed, 29 Jul 2026 13:06:54 +0000 Subject: [PATCH 4/5] test: assert the returned price and decimals in the overflow test The overflow test proved only that the call does not panic; a wrong non-reverting result would still have passed. Capture both return values and pin them to the mocked price. Mutation-validated: swapping the return to (decimals, price) fails the test (assertion failed: 5 != 1); restoring it passes (256 runs). Co-Authored-By: Claude --- test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol b/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol index f7357de..9bbd8df 100644 --- a/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol +++ b/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol @@ -261,8 +261,11 @@ contract LibOpFtsoCurrentPriceUsdTest is FtsoTest { mockPriceDetails(priceDetails); mockPrice(FTSO, currentPrice); - // timeout = max → priceTimestamp + timeout overflows; must not panic. - LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd(symbol, type(uint256).max); + // timeout = max → priceTimestamp + timeout overflows; must not panic, and + // the price is treated as fresh, so the mocked values come straight back. + (uint256 price, uint256 decimals) = LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd(symbol, type(uint256).max); + assertEq(price, currentPrice.price); + assertEq(decimals, currentPrice.decimals); } /// A negative Float timeout must revert with NegativeFixedDecimalConversion. From d23df287b997a4cfbbd9982b3a41feb62d477fda Mon Sep 17 00:00:00 2001 From: David Meister Date: Wed, 29 Jul 2026 14:03:16 +0000 Subject: [PATCH 5/5] ci: empty commit to re-test the Flare fork RPC after the secret update [3b-attempt]