From be421a78f918beea85975ae1c26bd740b25d9929 Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 29 Jun 2026 13:28:41 +0000 Subject: [PATCH 1/4] fix(lib): move DecimalsTooLarge guard into LibFtsoCurrentPriceUsd (#79) Any direct caller of LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd that forgets to bound the returned decimals before feeding them to a fixed- decimal scaler could silently mishandle a malicious FTSO. Moving the guard to the library enforces the bound at the trust boundary for every caller, and narrows the return type to (uint256, uint8) so Solidity makes the invariant machine-checkable at the call site. The op layer no longer needs its own DecimalsTooLarge check or import. Co-Authored-By: Claude Sonnet 4.6 --- src/lib/op/LibOpFtsoCurrentPriceUsd.sol | 10 ++-------- src/lib/price/LibFtsoCurrentPriceUsd.sol | 12 +++++++++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lib/op/LibOpFtsoCurrentPriceUsd.sol b/src/lib/op/LibOpFtsoCurrentPriceUsd.sol index ba6076d..fb373d9 100644 --- a/src/lib/op/LibOpFtsoCurrentPriceUsd.sol +++ b/src/lib/op/LibOpFtsoCurrentPriceUsd.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.19; import {OperandV2, StackItem} from "rain-interpreter-interface-0.1.0/src/interface/IInterpreterV4.sol"; import {LibIntOrAString, IntOrAString} from "rain-intorastring-0.1.0/src/lib/LibIntOrAString.sol"; import {LibFtsoCurrentPriceUsd} from "../price/LibFtsoCurrentPriceUsd.sol"; -import {DecimalsTooLarge} from "../../err/ErrFtso.sol"; import {LibDecimalFloat, Float} from "rain-math-float-0.1.1/src/lib/LibDecimalFloat.sol"; @@ -44,15 +43,10 @@ library LibOpFtsoCurrentPriceUsd { timeout := mload(add(inputs, 0x40)) } - (uint256 price, uint256 decimals) = LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd( + (uint256 price, uint8 decimals) = LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd( symbol.toStringV3(), LibDecimalFloat.toFixedDecimalLossless(timeout, 0) ); - if (decimals > type(uint8).max) { - revert DecimalsTooLarge(decimals); - } - // Check above ensures safe downcast. - //forge-lint: disable-next-line(unsafe-typecast) - Float priceFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(price, uint8(decimals)); + Float priceFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(price, decimals); StackItem[] memory outputs; assembly ("memory-safe") { diff --git a/src/lib/price/LibFtsoCurrentPriceUsd.sol b/src/lib/price/LibFtsoCurrentPriceUsd.sol index 7cded63..d725b2d 100644 --- a/src/lib/price/LibFtsoCurrentPriceUsd.sol +++ b/src/lib/price/LibFtsoCurrentPriceUsd.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.19; import {IFtsoRegistry, LibFlareContractRegistry} from "../registry/LibFlareContractRegistry.sol"; -import {InactiveFtso, PriceNotFinalized, StalePrice, InconsistentFtso} from "../../err/ErrFtso.sol"; +import {InactiveFtso, PriceNotFinalized, StalePrice, InconsistentFtso, DecimalsTooLarge} from "../../err/ErrFtso.sol"; import {IFtso} from "../../vendor/flare-smart-contracts/userInterfaces/IFtso.sol"; library LibFtsoCurrentPriceUsd { - function ftsoCurrentPriceUsd(string memory symbol, uint256 timeout) internal view returns (uint256, uint256) { + function ftsoCurrentPriceUsd(string memory symbol, uint256 timeout) internal view returns (uint256, uint8) { // Fetch the FTSO from the registry. IFtsoRegistry ftsoRegistry = LibFlareContractRegistry.getFtsoRegistry(); IFtso ftso = ftsoRegistry.getFtsoBySymbol(symbol); @@ -45,12 +45,18 @@ library LibFtsoCurrentPriceUsd { revert InconsistentFtso(); } + if (decimals > type(uint8).max) { + revert DecimalsTooLarge(decimals); + } + // Handle stale prices. //slither-disable-next-line timestamp if (block.timestamp > priceTimestamp + timeout) { revert StalePrice(priceTimestamp, timeout); } - return (price, decimals); + // Guard above ensures safe downcast. + //forge-lint: disable-next-line(unsafe-typecast) + return (price, uint8(decimals)); } } From 60cb6429ee6d1685c222b62ec14bd2bc47a8e241 Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 29 Jun 2026 17:14:28 +0000 Subject: [PATCH 2/4] fix(ci): copy-artifacts rainix-sol-test [3b-attempt]: regenerate pointers + bound testRunStale decimals Regenerate FlareFtsoWords.pointers.sol after guard moved to library changes function pointer offsets. Bound testRunStale decimals to uint8 range so the test exclusively exercises the staleness path (DecimalsTooLarge is separately covered by testRunDecimalOverflow). Co-Authored-By: Claude --- src/generated/FlareFtsoWords.pointers.sol | 8 ++++---- test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/generated/FlareFtsoWords.pointers.sol b/src/generated/FlareFtsoWords.pointers.sol index 5fe54d7..b6d7e36 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(0x304a801183c6e53c69a23533193e01b83cf2595a1f455d5463b0aa8295fd791c); /// @dev The hash of the meta that describes the contract. bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0x8717d07737e3cedcdddea6cd3337ae762d7089918bf8d818fb0afc5b63e3985a); @@ -48,13 +48,13 @@ bytes constant SUB_PARSER_WORD_PARSERS = hex"093509550967"; /// @dev Every two bytes is a function pointer for an operand handler. /// These positional indexes all map to the same indexes looked up in the parse /// meta. -bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = hex"0e430e430e43"; +bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = hex"0e040e040e04"; /// @dev The function pointers for the integrity check fns. -bytes constant INTEGRITY_FUNCTION_POINTERS = hex"0e200e2c0e38"; +bytes constant INTEGRITY_FUNCTION_POINTERS = hex"0de10ded0df9"; /// @dev The function pointers known to the interpreter for dynamic dispatch. /// By setting these as a constant they can be inlined into the interpreter /// and loaded at eval time for very low gas (~100) due to the compiler /// optimising it to a single `codecopy` to build the in memory bytes array. -bytes constant OPCODE_FUNCTION_POINTERS = hex"0a090ac70b1c"; +bytes constant OPCODE_FUNCTION_POINTERS = hex"0a090a880add"; diff --git a/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol b/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol index b48f5e2..1b1ab23 100644 --- a/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol +++ b/test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol @@ -139,6 +139,7 @@ contract LibOpFtsoCurrentPriceUsdTest is FtsoTest { ) external { vm.assume(bytes(symbol).length <= 31); uint256 intSymbol = IntOrAString.unwrap(LibIntOrAString.fromStringV3(symbol)); + currentPrice.decimals = bound(currentPrice.decimals, 0, type(uint8).max); timeout = bound(timeout, 0, uint256(int256(type(int224).max))); currentPrice.timestamp = bound(currentPrice.timestamp, 0, type(uint256).max - timeout - 1); From 1bcd080c08ff95b802f32922a91afc6fac579da3 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 30 Jun 2026 09:36:59 +0000 Subject: [PATCH 3/4] fix(ci): [3b-attempt] empty-commit retrigger (copy-artifacts meta + Ankr fork flakes; local meta matches committed) From 21c1a3d31435d573104016b81d0156798bc933e7 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 30 Jun 2026 09:58:04 +0000 Subject: [PATCH 4/4] fix(generated): [3b-attempt] update BYTECODE_HASH to CI-computed value after DecimalsTooLarge guard move CI reported expected 0xa30dc38f...; prior attempt committed hash from wrong nix shell. --- 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 b6d7e36..211c586 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(0x304a801183c6e53c69a23533193e01b83cf2595a1f455d5463b0aa8295fd791c); +bytes32 constant BYTECODE_HASH = bytes32(0xa30dc38f88990d4b3d866010bd823ce2da04588e589babd64435f2b4ebbddc0c); /// @dev The hash of the meta that describes the contract. bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0x8717d07737e3cedcdddea6cd3337ae762d7089918bf8d818fb0afc5b63e3985a);