From 9bb7465dadcf1b907412913ef82a928e93309bc4 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 16 Jun 2026 20:29:54 +0000 Subject: [PATCH 1/6] fix: revert with ContractNotRegistered on zero-address registry lookup IFlareContractRegistry documents address(0) as the not-found sentinel. The three getters were silently propagating a zero-typed contract handle, so every downstream call would revert with a confusing low-level error rather than a clear ContractNotRegistered domain error. Adds ContractNotRegistered(string name) custom error and reverts from all three getters if the resolved address is zero. Closes #45 Co-Authored-By: Claude --- src/lib/registry/LibFlareContractRegistry.sol | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/lib/registry/LibFlareContractRegistry.sol b/src/lib/registry/LibFlareContractRegistry.sol index 0451c8c..80d00c5 100644 --- a/src/lib/registry/LibFlareContractRegistry.sol +++ b/src/lib/registry/LibFlareContractRegistry.sol @@ -28,23 +28,38 @@ string constant FTSO_V2_LTS_NAME = "FtsoV2"; /// contract registry. string constant FEE_CALCULATOR_NAME = "FeeCalculator"; +/// @notice Thrown when a name lookup in the Flare contract registry returns +/// address(0). IFlareContractRegistry documents address(0) as the not-found +/// sentinel; propagating a zero-typed contract handle silently would cause +/// every subsequent call to revert with a confusing low-level error. +/// @param name The registry name that resolved to address(0). +error ContractNotRegistered(string name); + library LibFlareContractRegistry { /// Sugar for getting the FTSO registry address from the Flare contract - /// registry. + /// registry. Reverts with ContractNotRegistered if the name is not found. function getFtsoRegistry() internal view returns (IFtsoRegistry) { - return IFtsoRegistry(FLARE_CONTRACT_REGISTRY.getContractAddressByName(FTSO_REGISTRY_NAME)); + address addr = FLARE_CONTRACT_REGISTRY.getContractAddressByName(FTSO_REGISTRY_NAME); + if (addr == address(0)) revert ContractNotRegistered(FTSO_REGISTRY_NAME); + return IFtsoRegistry(addr); } /// Sugar for getting the FTSO V2 LTS contract address from the Flare - /// contract registry. + /// contract registry. Reverts with ContractNotRegistered if the name is not + /// found. //forge-lint: disable-next-line(mixed-case-function) function getFtsoV2LTS() internal view returns (FtsoV2Interface) { - return FtsoV2Interface(FLARE_CONTRACT_REGISTRY.getContractAddressByName(FTSO_V2_LTS_NAME)); + address addr = FLARE_CONTRACT_REGISTRY.getContractAddressByName(FTSO_V2_LTS_NAME); + if (addr == address(0)) revert ContractNotRegistered(FTSO_V2_LTS_NAME); + return FtsoV2Interface(addr); } /// Sugar for getting the FeeCalculator contract address from the Flare - /// contract registry. + /// contract registry. Reverts with ContractNotRegistered if the name is not + /// found. function getFeeCalculator() internal view returns (IFeeCalculator) { - return IFeeCalculator(FLARE_CONTRACT_REGISTRY.getContractAddressByName(FEE_CALCULATOR_NAME)); + address addr = FLARE_CONTRACT_REGISTRY.getContractAddressByName(FEE_CALCULATOR_NAME); + if (addr == address(0)) revert ContractNotRegistered(FEE_CALCULATOR_NAME); + return IFeeCalculator(addr); } } From ae1452169276f1e1de93596db5dbebeb71985a04 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 16 Jun 2026 22:21:54 +0000 Subject: [PATCH 2/6] 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..707a4af 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(0xf3e02b6af861f8a53f38d82e0e65fbde244d3b37c493150a817425bf94e11c47); /// @dev The hash of the meta that describes the contract. bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0x8717d07737e3cedcdddea6cd3337ae762d7089918bf8d818fb0afc5b63e3985a); From 9731cf9c7a133b36586884e61f87126f2c82166e Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 16 Jun 2026 22:51:29 +0000 Subject: [PATCH 3/6] test: pin ContractNotRegistered zero-address revert branch Add discriminating tests for the three LibFlareContractRegistry getters asserting they revert with ContractNotRegistered(name) when the Flare contract registry resolves the canonical name to address(0) (the documented not-found sentinel). vm.mockCall stubs getContractAddressByName to return address(0); an external harness wrapper gives each internal getter its own call frame so vm.expectRevert can observe the revert. Verified discriminating: passes with the guard present, fails (no revert) with the guard removed. Co-Authored-By: Claude Opus 4.8 --- .../registry/LibFlareContractRegistry.t.sol | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/test/src/lib/registry/LibFlareContractRegistry.t.sol b/test/src/lib/registry/LibFlareContractRegistry.t.sol index 892ec5c..0aeaaa3 100644 --- a/test/src/lib/registry/LibFlareContractRegistry.t.sol +++ b/test/src/lib/registry/LibFlareContractRegistry.t.sol @@ -4,11 +4,39 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {LibFork} from "test/fork/LibFork.sol"; -import {LibFlareContractRegistry, IFtsoRegistry} from "src/lib/registry/LibFlareContractRegistry.sol"; +import { + LibFlareContractRegistry, + IFtsoRegistry, + FLARE_CONTRACT_REGISTRY, + FTSO_REGISTRY_NAME, + FTSO_V2_LTS_NAME, + FEE_CALCULATOR_NAME, + ContractNotRegistered +} from "src/lib/registry/LibFlareContractRegistry.sol"; uint256 constant BLOCK_NUMBER = 31843105; +/// External wrapper around the internal library getters so that calling them via +/// `this.()` produces a dedicated external call frame. `vm.expectRevert` +/// asserts the *next call frame* reverts; an inlined internal library call has +/// no frame of its own, so the wrapper is required to observe the revert. +contract LibFlareContractRegistryExternal { + function getFtsoRegistry() external view returns (address) { + return address(LibFlareContractRegistry.getFtsoRegistry()); + } + + function getFtsoV2LTS() external view returns (address) { + return address(LibFlareContractRegistry.getFtsoV2LTS()); + } + + function getFeeCalculator() external view returns (address) { + return address(LibFlareContractRegistry.getFeeCalculator()); + } +} + contract LibFlareContractRegistryTest is Test { + LibFlareContractRegistryExternal internal external_ = new LibFlareContractRegistryExternal(); + constructor() { vm.createSelectFork(LibFork.rpcUrlFlare(vm), BLOCK_NUMBER); } @@ -17,4 +45,42 @@ contract LibFlareContractRegistryTest is Test { IFtsoRegistry ftsoRegistry = LibFlareContractRegistry.getFtsoRegistry(); assertEq(address(ftsoRegistry), address(0x13DC2b5053857AE17a4f95aFF55530b267F3E040)); } + + /// When the Flare contract registry resolves the FtsoRegistry name to + /// address(0) (the documented not-found sentinel), getFtsoRegistry MUST + /// revert with the specific ContractNotRegistered error rather than + /// returning a zero-typed handle. Removing the guard in the library makes + /// this test fail (the getter would return address(0) instead of + /// reverting). + function testGetFtsoRegistryZeroAddressReverts() external { + vm.mockCall( + address(FLARE_CONTRACT_REGISTRY), + abi.encodeWithSelector(FLARE_CONTRACT_REGISTRY.getContractAddressByName.selector, FTSO_REGISTRY_NAME), + abi.encode(address(0)) + ); + vm.expectRevert(abi.encodeWithSelector(ContractNotRegistered.selector, FTSO_REGISTRY_NAME)); + external_.getFtsoRegistry(); + } + + /// As above, for the FtsoV2 LTS getter. + function testGetFtsoV2LTSZeroAddressReverts() external { + vm.mockCall( + address(FLARE_CONTRACT_REGISTRY), + abi.encodeWithSelector(FLARE_CONTRACT_REGISTRY.getContractAddressByName.selector, FTSO_V2_LTS_NAME), + abi.encode(address(0)) + ); + vm.expectRevert(abi.encodeWithSelector(ContractNotRegistered.selector, FTSO_V2_LTS_NAME)); + external_.getFtsoV2LTS(); + } + + /// As above, for the FeeCalculator getter. + function testGetFeeCalculatorZeroAddressReverts() external { + vm.mockCall( + address(FLARE_CONTRACT_REGISTRY), + abi.encodeWithSelector(FLARE_CONTRACT_REGISTRY.getContractAddressByName.selector, FEE_CALCULATOR_NAME), + abi.encode(address(0)) + ); + vm.expectRevert(abi.encodeWithSelector(ContractNotRegistered.selector, FEE_CALCULATOR_NAME)); + external_.getFeeCalculator(); + } } From 3ac5d15573af1e3f6535c135f2c51b78f86ae29b Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 18 Jun 2026 01:14:55 +0000 Subject: [PATCH 4/6] fix(ci): rainix-sol-single-contract [3b-attempt] Split LibFlareContractRegistryExternal into its own file so each .sol has exactly one contract, satisfying the rainix one-contract-per-file static check. Co-Authored-By: Claude --- .../registry/LibFlareContractRegistry.t.sol | 20 ++------------ .../LibFlareContractRegistryExternal.sol | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 test/src/lib/registry/LibFlareContractRegistryExternal.sol diff --git a/test/src/lib/registry/LibFlareContractRegistry.t.sol b/test/src/lib/registry/LibFlareContractRegistry.t.sol index 0aeaaa3..1580d2d 100644 --- a/test/src/lib/registry/LibFlareContractRegistry.t.sol +++ b/test/src/lib/registry/LibFlareContractRegistry.t.sol @@ -13,27 +13,11 @@ import { FEE_CALCULATOR_NAME, ContractNotRegistered } from "src/lib/registry/LibFlareContractRegistry.sol"; +import {LibFlareContractRegistryExternal} from + "test/src/lib/registry/LibFlareContractRegistryExternal.sol"; uint256 constant BLOCK_NUMBER = 31843105; -/// External wrapper around the internal library getters so that calling them via -/// `this.()` produces a dedicated external call frame. `vm.expectRevert` -/// asserts the *next call frame* reverts; an inlined internal library call has -/// no frame of its own, so the wrapper is required to observe the revert. -contract LibFlareContractRegistryExternal { - function getFtsoRegistry() external view returns (address) { - return address(LibFlareContractRegistry.getFtsoRegistry()); - } - - function getFtsoV2LTS() external view returns (address) { - return address(LibFlareContractRegistry.getFtsoV2LTS()); - } - - function getFeeCalculator() external view returns (address) { - return address(LibFlareContractRegistry.getFeeCalculator()); - } -} - contract LibFlareContractRegistryTest is Test { LibFlareContractRegistryExternal internal external_ = new LibFlareContractRegistryExternal(); diff --git a/test/src/lib/registry/LibFlareContractRegistryExternal.sol b/test/src/lib/registry/LibFlareContractRegistryExternal.sol new file mode 100644 index 0000000..1e7253e --- /dev/null +++ b/test/src/lib/registry/LibFlareContractRegistryExternal.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import { + LibFlareContractRegistry, + IFtsoRegistry +} from "src/lib/registry/LibFlareContractRegistry.sol"; + +/// External wrapper around the internal library getters so that calling them via +/// `this.()` produces a dedicated external call frame. `vm.expectRevert` +/// asserts the *next call frame* reverts; an inlined internal library call has +/// no frame of its own, so the wrapper is required to observe the revert. +contract LibFlareContractRegistryExternal { + function getFtsoRegistry() external view returns (address) { + return address(LibFlareContractRegistry.getFtsoRegistry()); + } + + function getFtsoV2LTS() external view returns (address) { + return address(LibFlareContractRegistry.getFtsoV2LTS()); + } + + function getFeeCalculator() external view returns (address) { + return address(LibFlareContractRegistry.getFeeCalculator()); + } +} From 170089483880bf8ae6cdc11b24aaffeab627e973 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 18 Jun 2026 09:49:10 +0000 Subject: [PATCH 5/6] fix(ci): copy-artifacts [3b-attempt] Apply forge fmt to the registry test files added/modified by prior commits. The split of LibFlareContractRegistryExternal.sol was not forge fmt'ed before commit, causing the copy-artifacts fmt step to dirty the tree. Note: rainix-sol/static/static remains red on the slither naming-convention error in src/interface/IDineroFlrEth.sol (slither-disable-start inside interface causes parse error; needs human fix via next-line or per-file config). Co-Authored-By: Claude --- test/src/lib/registry/LibFlareContractRegistry.t.sol | 3 +-- test/src/lib/registry/LibFlareContractRegistryExternal.sol | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/test/src/lib/registry/LibFlareContractRegistry.t.sol b/test/src/lib/registry/LibFlareContractRegistry.t.sol index 1580d2d..6044eef 100644 --- a/test/src/lib/registry/LibFlareContractRegistry.t.sol +++ b/test/src/lib/registry/LibFlareContractRegistry.t.sol @@ -13,8 +13,7 @@ import { FEE_CALCULATOR_NAME, ContractNotRegistered } from "src/lib/registry/LibFlareContractRegistry.sol"; -import {LibFlareContractRegistryExternal} from - "test/src/lib/registry/LibFlareContractRegistryExternal.sol"; +import {LibFlareContractRegistryExternal} from "test/src/lib/registry/LibFlareContractRegistryExternal.sol"; uint256 constant BLOCK_NUMBER = 31843105; diff --git a/test/src/lib/registry/LibFlareContractRegistryExternal.sol b/test/src/lib/registry/LibFlareContractRegistryExternal.sol index 1e7253e..3b07d34 100644 --- a/test/src/lib/registry/LibFlareContractRegistryExternal.sol +++ b/test/src/lib/registry/LibFlareContractRegistryExternal.sol @@ -2,10 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import { - LibFlareContractRegistry, - IFtsoRegistry -} from "src/lib/registry/LibFlareContractRegistry.sol"; +import {LibFlareContractRegistry, IFtsoRegistry} from "src/lib/registry/LibFlareContractRegistry.sol"; /// External wrapper around the internal library getters so that calling them via /// `this.()` produces a dedicated external call frame. `vm.expectRevert` From e2abb4b1eba74122c18461c4fe38dcbe4c34c75d Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 19 Jun 2026 17:20:15 +0000 Subject: [PATCH 6/6] merge(main): resolve conflicts [merge-update]