From 6f1bd8236115e8ce20b63b6db27eac51ff7904d1 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 23 Jun 2026 10:22:43 +0000 Subject: [PATCH 1/2] fix(sflr): revert on zero rate from sFLR contract Adds ZeroSFLRRate error (ErrFtso.sol) and a guard in getSFLRPerFLR18(): if the sFLR contract returns 0 (e.g. uninitialized), revert instead of silently forwarding a zero exchange rate to oracle consumers. Adds a unit test using vm.mockCall to verify the zero-rate revert. Regenerated FlareFtsoWords.pointers.sol (new bytecode hash). Closes #80 Co-Authored-By: Claude Sonnet 4.6 --- src/err/ErrFtso.sol | 3 +++ src/generated/FlareFtsoWords.pointers.sol | 2 +- src/lib/sflr/LibSceptreStakedFlare.sol | 5 ++++- test/src/lib/sflr/LibSceptreStakedFlare.t.sol | 14 +++++++++++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/err/ErrFtso.sol b/src/err/ErrFtso.sol index 83b4c05..6e4c9a1 100644 --- a/src/err/ErrFtso.sol +++ b/src/err/ErrFtso.sol @@ -28,3 +28,6 @@ error InconsistentFtso(); /// Thrown when decimals are too large to handle safely. /// @param decimals The decimals that were too large. error DecimalsTooLarge(uint256 decimals); + +/// Thrown when the sFLR contract returns a zero exchange rate. +error ZeroSFLRRate(); diff --git a/src/generated/FlareFtsoWords.pointers.sol b/src/generated/FlareFtsoWords.pointers.sol index 5fe54d7..e2f4850 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(0xa6e08b45005271ce77af0810fef0a97d7bbbdda007793e4be428ba5737caf303); /// @dev The hash of the meta that describes the contract. bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0x8717d07737e3cedcdddea6cd3337ae762d7089918bf8d818fb0afc5b63e3985a); diff --git a/src/lib/sflr/LibSceptreStakedFlare.sol b/src/lib/sflr/LibSceptreStakedFlare.sol index ed0ddbc..e971c95 100644 --- a/src/lib/sflr/LibSceptreStakedFlare.sol +++ b/src/lib/sflr/LibSceptreStakedFlare.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; import {IStakedFlr} from "../../interface/IStakedFlr.sol"; +import {ZeroSFLRRate} from "../../err/ErrFtso.sol"; /// @dev Immutable upgradeable proxy contract to the sFLR contract. IStakedFlr constant SFLR_CONTRACT = IStakedFlr(address(0x12e605bc104e93B45e1aD99F9e555f659051c2BB)); @@ -13,6 +14,8 @@ library LibSceptreStakedFlare { /// according to the sFLR contract's current exchange rate. //forge-lint: disable-next-line(mixed-case-function) function getSFLRPerFLR18() internal view returns (uint256) { - return SFLR_CONTRACT.getSharesByPooledFlr(1e18); + uint256 rate = SFLR_CONTRACT.getSharesByPooledFlr(1e18); + if (rate == 0) revert ZeroSFLRRate(); + return rate; } } diff --git a/test/src/lib/sflr/LibSceptreStakedFlare.t.sol b/test/src/lib/sflr/LibSceptreStakedFlare.t.sol index cf91014..626fbcd 100644 --- a/test/src/lib/sflr/LibSceptreStakedFlare.t.sol +++ b/test/src/lib/sflr/LibSceptreStakedFlare.t.sol @@ -4,7 +4,9 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {LibFork} from "test/fork/LibFork.sol"; -import {LibSceptreStakedFlare} from "src/lib/sflr/LibSceptreStakedFlare.sol"; +import {LibSceptreStakedFlare, SFLR_CONTRACT} from "src/lib/sflr/LibSceptreStakedFlare.sol"; +import {IStakedFlr} from "src/interface/IStakedFlr.sol"; +import {ZeroSFLRRate} from "src/err/ErrFtso.sol"; uint256 constant BLOCK_NUMBER = 31843105; @@ -17,4 +19,14 @@ contract LibSceptreStakedFlareTest is Test { uint256 rate18 = LibSceptreStakedFlare.getSFLRPerFLR18(); assertEq(rate18, 0.877817288626455057e18); } + + function testGetSFLRPerFLR18ZeroReverts() external { + vm.mockCall( + address(SFLR_CONTRACT), + abi.encodeWithSelector(IStakedFlr.getSharesByPooledFlr.selector, uint256(1e18)), + abi.encode(uint256(0)) + ); + vm.expectRevert(abi.encodeWithSelector(ZeroSFLRRate.selector)); + LibSceptreStakedFlare.getSFLRPerFLR18(); + } } From 32eb4c70e684c0577450cc15bb822d0134835b04 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 23 Jun 2026 10:38:53 +0000 Subject: [PATCH 2/2] fix(ci): use this.externalGetSFLRPerFLR18 so vm.expectRevert catches the right call [3b-attempt] --- test/src/lib/sflr/LibSceptreStakedFlare.t.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/src/lib/sflr/LibSceptreStakedFlare.t.sol b/test/src/lib/sflr/LibSceptreStakedFlare.t.sol index 626fbcd..9a415f7 100644 --- a/test/src/lib/sflr/LibSceptreStakedFlare.t.sol +++ b/test/src/lib/sflr/LibSceptreStakedFlare.t.sol @@ -15,6 +15,10 @@ contract LibSceptreStakedFlareTest is Test { vm.createSelectFork(LibFork.rpcUrlFlare(vm), BLOCK_NUMBER); } + function externalGetSFLRPerFLR18() external view returns (uint256) { + return LibSceptreStakedFlare.getSFLRPerFLR18(); + } + function testGetSFLRPerFLR18() external view { uint256 rate18 = LibSceptreStakedFlare.getSFLRPerFLR18(); assertEq(rate18, 0.877817288626455057e18); @@ -27,6 +31,6 @@ contract LibSceptreStakedFlareTest is Test { abi.encode(uint256(0)) ); vm.expectRevert(abi.encodeWithSelector(ZeroSFLRRate.selector)); - LibSceptreStakedFlare.getSFLRPerFLR18(); + this.externalGetSFLRPerFLR18(); } }