Skip to content

fix(build): disable forge remapping auto-detection - #1364

Merged
Maikol merged 4 commits into
mainfrom
fix/foundry-remapping-contexts
Oct 1, 2026
Merged

Maikol merged 4 commits into
mainfrom
fix/foundry-remapping-contexts

Conversation

@Maikol

@Maikol Maikol commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Problem

Build and Test fails on every PR at the "Build all packages" step, while compiling packages/horizon:

Error in plugin hardhat-foundry: Invalid remapping
'node_modules/hardhat-graph-protocol/node_modules/@graphprotocol/toolshed/node_modules/@graphprotocol/issuance/node_modules/@graphprotocol/interfaces/:hardhat/=...',
remapping contexts are not allowed

This is not specific to any branch — it also hit tmigone/reo-data-edge (run 36451132406) and deployment/mainnet/2026-09-18/security-fixes (run 36624310497).

Root cause

  1. .github/actions/setup installs Foundry via foundry-rs/foundry-toolchain@v1 with no version pin, so CI gets whatever stable currently is (today forge 1.8.3).
  2. Forge 1.8.x recurses into nested node_modules when auto-detecting remappings and emits context-qualified entries of the form path/:prefix/=target. In packages/horizon it produces 15 of them.
  3. @nomicfoundation/hardhat-foundry rejects any remapping containing : (foundry.ts: if (remappingLine.includes(":")) throw new HardhatFoundryError(...)), so hardhat compile aborts and pnpm -r build fails.

Measured on the same checkout, same node_modules:

forge context remappings in packages/horizon
1.5.1 0
1.8.3 (CI's stable) 15

The nested node_modules layout is not new, and it is identical locally — the only variable is the Forge version.

Fix

Set auto_detect_remappings = false in foundry.toml for the two packages that use the plugin (horizon, subgraph-service). Both already declare every remapping they need in remappings.txt:

@openzeppelin/=node_modules/@openzeppelin/
@graphprotocol/=node_modules/@graphprotocol/
forge-std/=node_modules/forge-std/src/

The only mapping auto-detection contributed beyond those was hardhat/=node_modules/hardhat/, and no Solidity file in either package imports hardhat/. So auto-detection added nothing except the entries that break the plugin.

Upgrading the plugin is not an alternative: 1.2.1 contains the same includes(":") throw, and 3.x targets 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 in horizon and subgraph-service (this is the step that was failing)
  • forge build --force — 240 files in horizon, 222 in subgraph-service, both clean
  • forge test in horizon — 574 passed, 0 failed, 2 skipped

Before the change, npx hardhat compile with 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 from packageManager — 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 to v1.8.3 therefore changes nothing about what CI runs right now; it only stops the next release landing unannounced on an unrelated PR.

- name: Install Foundry
  uses: foundry-rs/foundry-toolchain@v1
  with:
    version: v1.8.3

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 the uses: 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: isolate

With the remapping fix in place, Build all packages passes and CI moves on to Test all packages, which then failed in packages/issuance:

[FAIL: afterCollection (canceled by SP) exceeds 1/10th of callback gas budget: 219858 >= 150000]
test_AfterCollection_GasWithinBudget_CanceledBySP()

Also not branch-specific — it reproduces on main with no changes.

Cause: forge 1.8 flipped the default of isolate from false to true. Isolate mode runs every top-level call as its own transaction, charging intrinsic gas and cold-account/storage access. The canaries in test/unit/agreement-manager/callbackGas.t.sol measure a callback that RecurringCollector invokes inside a transaction, so the isolated measurement overstates it and crosses the 150k alarm threshold.

Measured for the four canaries on the same checkout:

escrow sufficient JIT deposit full reconcile canceled by SP
forge 1.5.1 9,796 43,466 50,476 91,846
forge 1.8.3, isolate on (new default) 45,717 135,609 148,799 219,195
forge 1.8.3, isolate off 9,133 42,803 49,813 91,183

Note full reconcile lands at 148,799 against a 150,000 threshold — it would have started failing on the next small change regardless.

Fix: pin isolate = false in packages/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:

  • The vm.prank inside the measured region is not the cause (moving it changes the result by ~660 gas).
  • evm_version is not the cause: both versions resolve cancun.
  • 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 test in packages/issuance with forge 1.8.3 and this fix — 622 passed, 0 failed (was 621 passed, 1 failed).


Third regression from the same bump: FOUNDRY_COVERAGE

With the first two fixes in, Build all packages and Test all packages both pass and CI reaches Test with coverage, which failed in packages/horizon:

Error: failed to extract foundry config:
foundry config error: invalid type: found unsigned int `1`, expected struct CoverageConfig for setting `coverage`

Cause: forge 1.8 added a [coverage] config table. Foundry maps FOUNDRY_* env vars onto config keys, so FOUNDRY_COVERAGE=1 — set by horizon's coverage scripts — is now parsed as the coverage key 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=1 from the two scripts in packages/horizon/package.json. Nothing in the repo reads it (those two scripts are its only references anywhere), and issuance and subgraph-service already run plain forge coverage, so this just makes horizon consistent.

Verification: forge coverage --report lcov in packages/horizon with forge 1.8.3 completes — 574 passed, 2 skipped — and writes coverage/lcov.info.


Summary

Three independent breakages, all from CI's Foundry moving from 1.5.x to 1.8.x on an unpinned action:

# Symptom Forge 1.8 change Fix
1 hardhat compile fails on an invalid remapping auto-detection now recurses into nested node_modules and emits context-qualified remappings auto_detect_remappings = false in horizon + subgraph-service
2 issuance gas canary exceeds its budget isolate default flipped to true isolate = false in issuance
3 forge coverage refuses to start new [coverage] config table collides with FOUNDRY_COVERAGE drop the stale env var from horizon's scripts

Each was reproduced on main with 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.

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.47%. Comparing base (160e6bc) to head (6f1bef2).
⚠️ Report is 18 commits behind head on main.

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     
Flag Coverage Δ
unittests 91.47% <ø> (+0.31%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Maikol added 3 commits October 1, 2026 11:44
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.
@Maikol
Maikol force-pushed the fix/foundry-remapping-contexts branch from 11d2abd to 15630b9 Compare October 1, 2026 14:45
tmigone
tmigone previously approved these changes Oct 1, 2026
@cjorge-graphops

Copy link
Copy Markdown
Follow-up (not in this PR)

CI's Foundry version is still unpinned, so the next breaking change in stable will land the same way. Pinning it in .github/actions/setup would make builds deterministic:

- name: Install Foundry
  uses: foundry-rs/foundry-toolchain@v1
  with:
    version: v1.8.3

Left out deliberately, since it affects every workflow in the repo.

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.
@Maikol

Maikol commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

You're right, and I've pinned it in 6f1bef25.

Your premise checks out: Foundry is installed in exactly one place (.github/actions/setup), used by build-test, lint, publish and verifydeployed, and nothing else installs or pins it. So all four already receive 1.8.x stable today — pinning to v1.8.3 changes nothing about what CI runs now and only stops the next release landing unannounced on an unrelated PR. It also matches the rest of that action, which already pins node via .nvmrc and pnpm via packageManager; Foundry was the odd one out.

Two notes for the record:

  • The three config fixes still carry their weight with the pin in place. auto_detect_remappings and isolate are now explicit rather than inherited from forge's defaults, and FOUNDRY_COVERAGE was a variable nothing read. Pinning alone would have deferred all three to the next bump rather than resolved them.
  • Dependabot won't bump a with: version: input — it only tracks the uses: ref — so upgrading Foundry becomes a deliberate PR. That's the trade-off, and arguably the point: a failure then arrives attributable instead of spontaneously, which is what cost us this round.

@Maikol
Maikol merged commit 1c5f02a into main Oct 1, 2026
5 checks passed
@Maikol
Maikol deleted the fix/foundry-remapping-contexts branch October 1, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants