fix(build): disable forge remapping auto-detection - #1364
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1364 +/- ##
==========================================
+ Coverage 91.16% 91.47% +0.31%
==========================================
Files 81 81
Lines 5319 5326 +7
Branches 1128 973 -155
==========================================
+ Hits 4849 4872 +23
+ Misses 448 433 -15
+ Partials 22 21 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Forge 1.8.x walks nested node_modules when auto-detecting remappings and emits context-qualified entries (path/:prefix/=target). hardhat-foundry rejects any remapping containing ':', so 'hardhat compile' fails in horizon and subgraph-service: Error in plugin hardhat-foundry: Invalid remapping 'node_modules/.../@graphprotocol/interfaces/:hardhat/=...', remapping contexts are not allowed CI installs foundry via foundry-toolchain@v1 without a version pin, so it picked this up as soon as stable moved past 1.5.x. Both packages already declare every remapping they need in remappings.txt, and no Solidity imports 'hardhat/', so auto-detection adds nothing but the broken entries.
Forge 1.8 flipped the default of 'isolate' to true, which executes every top-level call as its own transaction and therefore charges intrinsic + cold-access gas. The callback gas canaries measure a callback that RecurringCollector invokes inside a transaction, so isolated measurement overstates it and trips the 150k budget: [FAIL: afterCollection (canceled by SP) exceeds 1/10th of callback gas budget: 219858 >= 150000] test_AfterCollection_GasWithinBudget_CanceledBySP() Measured for the four canaries, same checkout: forge 1.5.1 9796 / 43466 / 50476 / 91846 forge 1.8.3 isolate on 45717 / 135609 / 148799 / 219195 forge 1.8.3 isolate off 9133 / 42803 / 49813 / 91183 Pinning the setting keeps the measurement matched to how production calls the callback, and stable across forge versions.
Forge 1.8 added a [coverage] config table, so the FOUNDRY_COVERAGE env var now maps onto that key and forge refuses to start: Error: failed to extract foundry config: foundry config error: invalid type: found unsigned int `1`, expected struct CoverageConfig for setting `coverage` Nothing in the repo reads FOUNDRY_COVERAGE — these two scripts are its only references — and issuance and subgraph-service already run plain 'forge coverage'. Removing it makes horizon consistent with them. Verified: 'forge coverage --report lcov' in packages/horizon with forge 1.8.3 completes (574 passed, 2 skipped) and writes coverage/lcov.info.
11d2abd to
15630b9
Compare
I'm under the impression that the potentially affected workflows are subject to no-pinning as well, in which case they will be getting this version anyway? 1.8.x stable. Pinning solves something without bringing a regression? (that doesn't already exist given that). So I would argue to just pin right away :p |
Foundry was the only unpinned toolchain in the setup action — node comes from .nvmrc and pnpm from packageManager — and an unpinned bump from 1.5.x to 1.8.x broke CI three separate ways on unrelated PRs. All four workflows that install Foundry (build-test, lint, publish, verifydeployed) go through this action, so they already receive 1.8.x stable today; pinning to the version CI is currently running changes nothing now and only stops future releases landing unannounced on unrelated PRs.
|
You're right, and I've pinned it in Your premise checks out: Foundry is installed in exactly one place ( Two notes for the record:
|
Problem
Build and Testfails on every PR at the "Build all packages" step, while compilingpackages/horizon:This is not specific to any branch — it also hit
tmigone/reo-data-edge(run 36451132406) anddeployment/mainnet/2026-09-18/security-fixes(run 36624310497).Root cause
.github/actions/setupinstalls Foundry viafoundry-rs/foundry-toolchain@v1with no version pin, so CI gets whateverstablecurrently is (today forge 1.8.3).node_moduleswhen auto-detecting remappings and emits context-qualified entries of the formpath/:prefix/=target. Inpackages/horizonit produces 15 of them.@nomicfoundation/hardhat-foundryrejects any remapping containing:(foundry.ts:if (remappingLine.includes(":")) throw new HardhatFoundryError(...)), sohardhat compileaborts andpnpm -r buildfails.Measured on the same checkout, same
node_modules:packages/horizonstable)The nested
node_moduleslayout is not new, and it is identical locally — the only variable is the Forge version.Fix
Set
auto_detect_remappings = falseinfoundry.tomlfor the two packages that use the plugin (horizon,subgraph-service). Both already declare every remapping they need inremappings.txt:The only mapping auto-detection contributed beyond those was
hardhat/=node_modules/hardhat/, and no Solidity file in either package importshardhat/. So auto-detection added nothing except the entries that break the plugin.Upgrading the plugin is not an alternative:
1.2.1contains the sameincludes(":")throw, and3.xtargets Hardhat 3 while these packages are on Hardhat 2.Verification
All of the following were run locally with forge 1.8.3 (the version CI installs) and the fix applied:
forge remappings— 3 entries, 0 contexts (was 15)npx hardhat compile— passes inhorizonandsubgraph-service(this is the step that was failing)forge build --force— 240 files inhorizon, 222 insubgraph-service, both cleanforge testinhorizon— 574 passed, 0 failed, 2 skippedBefore the change,
npx hardhat compilewith forge 1.8.3 reproduces the CI error exactly.Pinning Foundry
CI's Foundry was the only unpinned toolchain in
.github/actions/setup— node comes from.nvmrc, pnpm frompackageManager— and this unpinned bump from 1.5.x to 1.8.x is what broke CI three separate ways on unrelated PRs.All four workflows that install Foundry (
build-test,lint,publish,verifydeployed) go through that one action, so they already receive 1.8.x stable today. Pinning tov1.8.3therefore changes nothing about what CI runs right now; it only stops the next release landing unannounced on an unrelated PR.The three config fixes above are still needed with the pin in place: they make the settings explicit rather than inherited from whatever forge defaults to, so they hold at any version. Pinning alone would just defer all three to the next bump.
Note that Dependabot won't bump a
with: version:input (it only tracks theuses:ref), so upgrading Foundry becomes a deliberate PR — which is the point: failures then arrive attributable instead of spontaneously.Second regression from the same unpinned bump:
isolateWith the remapping fix in place,
Build all packagespasses and CI moves on toTest all packages, which then failed inpackages/issuance:Also not branch-specific — it reproduces on
mainwith no changes.Cause: forge 1.8 flipped the default of
isolatefromfalsetotrue. Isolate mode runs every top-level call as its own transaction, charging intrinsic gas and cold-account/storage access. The canaries intest/unit/agreement-manager/callbackGas.t.solmeasure a callback thatRecurringCollectorinvokes inside a transaction, so the isolated measurement overstates it and crosses the 150k alarm threshold.Measured for the four canaries on the same checkout:
Note
full reconcilelands at 148,799 against a 150,000 threshold — it would have started failing on the next small change regardless.Fix: pin
isolate = falseinpackages/issuance/foundry.toml. This keeps the measurement matched to how production reaches the callback and makes it stable across forge versions, rather than raising the threshold and weakening the canary.Ruled out along the way:
vm.prankinside the measured region is not the cause (moving it changes the result by ~660 gas).evm_versionis not the cause: both versions resolvecancun.packages/testing/test/gas/CallbackGas.t.sol(the production-representative suite, 750k threshold) passes under isolate mode, so no change is needed there.Verification: full
forge testinpackages/issuancewith forge 1.8.3 and this fix — 622 passed, 0 failed (was 621 passed, 1 failed).Third regression from the same bump:
FOUNDRY_COVERAGEWith the first two fixes in,
Build all packagesandTest all packagesboth pass and CI reachesTest with coverage, which failed inpackages/horizon:Cause: forge 1.8 added a
[coverage]config table. Foundry mapsFOUNDRY_*env vars onto config keys, soFOUNDRY_COVERAGE=1— set by horizon's coverage scripts — is now parsed as thecoveragekey and rejected for being an integer rather than a table. On forge 1.5.1 the same variable is simply ignored.Fix: drop
FOUNDRY_COVERAGE=1from the two scripts inpackages/horizon/package.json. Nothing in the repo reads it (those two scripts are its only references anywhere), andissuanceandsubgraph-servicealready run plainforge coverage, so this just makes horizon consistent.Verification:
forge coverage --report lcovinpackages/horizonwith forge 1.8.3 completes — 574 passed, 2 skipped — and writescoverage/lcov.info.Summary
Three independent breakages, all from CI's Foundry moving from 1.5.x to 1.8.x on an unpinned action:
hardhat compilefails on an invalid remappingnode_modulesand emits context-qualified remappingsauto_detect_remappings = falsein horizon + subgraph-serviceisolatedefault flipped totrueisolate = falsein issuanceforge coveragerefuses to start[coverage]config table collides withFOUNDRY_COVERAGEEach was reproduced on
mainwith no source changes, using the exact forge version CI installs (1.8.3), and each fix was verified against that same version. None is specific to any branch.This also unblocks #1363, which is failing on the first of these.