From b3e5c259ddfee95be9c0808e0c05e84f23342965 Mon Sep 17 00:00:00 2001 From: David Meister Date: Sat, 15 Aug 2026 12:52:59 +0000 Subject: [PATCH] test: assert [rpc_endpoints] and [etherscan] are exactly supportedNetworks() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The supported-network list was spelled three times — supportedNetworks(), [rpc_endpoints] and [etherscan] — with nothing joining them. Only the RPC pairing was enforced, and only incidentally, by the fork tests failing on a name with no alias. Nothing anywhere read [etherscan], so a network added without an explorer key broadcast, spent the gas, and only then failed --verify with no API key configured for the chain. testSupportedNetworksAreFullyConfigured reads foundry.toml and asserts both sections against supportedNetworks() by membership in both directions, so the three lists can only be one list: a missing entry is the broadcast-then-fail, and a stray entry is config nothing reads. Reading the raw file rather than the resolved config keeps the ${VAR} values out of it, so the test needs no RPC and fails on the PR that drifts rather than at dispatch time. Closes #47 Co-Authored-By: Claude Opus 5 (1M context) --- foundry.toml | 4 ++ test/src/lib/LibRainDeploy.t.sol | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/foundry.toml b/foundry.toml index d2ba1a6..73a10dc 100644 --- a/foundry.toml +++ b/foundry.toml @@ -68,6 +68,10 @@ polygon = "${POLYGON_RPC_URL}" # these variable names, so a deploy without this section broadcasts and then # fails with no API key configured for the chain — after spending the gas. One # entry per `[rpc_endpoints]` alias, because the deploy goes to all of them. +# +# Both sections are checked against `LibRainDeploy.supportedNetworks()`, in both +# directions, by `testSupportedNetworksAreFullyConfigured`. Adding a network is +# an edit to all three or a red test, not a broadcast that discovers it. [etherscan] arbitrum = { key = "${CI_DEPLOY_ARBITRUM_ETHERSCAN_API_KEY}" } base = { key = "${CI_DEPLOY_BASE_ETHERSCAN_API_KEY}" } diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index 23d302c..2734826 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -203,6 +203,70 @@ contract LibRainDeployTest is Test { assertEq(networks[4], LibRainDeploy.POLYGON); } + /// Whether `names` holds `name`. A config section is keyed, not ordered, so + /// membership is the only thing worth asserting across one. + /// @param names The names to search. + /// @param name The name to look for. + /// @return Whether it is there. + function holdsName(string[] memory names, string memory name) internal pure returns (bool) { + for (uint256 i = 0; i < names.length; i++) { + if (keccak256(bytes(names[i])) == keccak256(bytes(name))) { + return true; + } + } + return false; + } + + /// PROPERTY: `[rpc_endpoints]` and `[etherscan]` in `foundry.toml` are + /// EXACTLY `supportedNetworks()`, which makes the three lists one list. + /// + /// The deploy forks by the first and `--verify` resolves the second, so a + /// supported network missing from either broadcasts and then fails after + /// the gas is spent, and a section entry no supported network names is + /// config nothing ever reads. Both are the same defect — the lists having + /// drifted — so both directions are asserted, by membership: containment + /// one way alone passes for a section carrying an alias nothing deploys + /// to, and the other way alone passes for a network with no config at all. + /// + /// This is what makes the `[etherscan]` half enforced at all. The RPC half + /// is enforced only incidentally, by the fork tests, and only forwards. + /// + /// The raw file is read rather than forge's resolved config because the + /// values are `${VAR}` interpolations that exist only in CI. The KEYS are + /// the whole contract here, and they are in the text — so this needs no + /// RPC and fails on the PR that drifts rather than at dispatch time. + function testSupportedNetworksAreFullyConfigured() external view { + string memory config = vm.readFile("foundry.toml"); + string[] memory networks = LibRainDeploy.supportedNetworks(); + + for (uint256 i = 0; i < networks.length; i++) { + assertTrue( + vm.keyExistsToml(config, string.concat(".rpc_endpoints.", networks[i])), + string.concat("supported network has no [rpc_endpoints] alias: ", networks[i]) + ); + assertTrue( + vm.keyExistsToml(config, string.concat(".etherscan.", networks[i])), + string.concat("supported network has no [etherscan] key: ", networks[i]) + ); + } + + string[] memory rpcAliases = vm.parseTomlKeys(config, ".rpc_endpoints"); + for (uint256 i = 0; i < rpcAliases.length; i++) { + assertTrue( + holdsName(networks, rpcAliases[i]), + string.concat("[rpc_endpoints] alias is not a supported network: ", rpcAliases[i]) + ); + } + + string[] memory etherscanKeys = vm.parseTomlKeys(config, ".etherscan"); + for (uint256 i = 0; i < etherscanKeys.length; i++) { + assertTrue( + holdsName(networks, etherscanKeys[i]), + string.concat("[etherscan] key is not a supported network: ", etherscanKeys[i]) + ); + } + } + /// `ZOLTU_FACTORY_CODEHASH` MUST match the actual codehash of the Zoltu /// factory on every supported network. Every name in `supportedNetworks` /// MUST also be a configured fork alias, otherwise it cannot be deployed