Skip to content

feat(contracts): add contract upgrade hygiene check#2108

Closed
Eikix wants to merge 13 commits intomainfrom
upgrade-ci-detection
Closed

feat(contracts): add contract upgrade hygiene check#2108
Eikix wants to merge 13 commits intomainfrom
upgrade-ci-detection

Conversation

@Eikix
Copy link
Copy Markdown
Contributor

@Eikix Eikix commented Mar 13, 2026

Summary

  • New lightweight CI workflow that compares PR contract bytecodes against main
  • Fails fast if bytecode changed without REINITIALIZER_VERSION bump, reinitializeVX function, or semantic version bump
  • Covers both host-contracts and gateway-contracts via shared ci/check-upgrade-hygiene.sh
  • Independent of existing upgrade-tests workflows (no Docker/Anvil needed)

Motivation

ACL.sol had logic changed (commit 803f104) without bumping the reinitializer version.
Existing CI only checks if REINITIALIZER_VERSION changed to decide whether to upgrade — it cannot detect bytecode changes without version bumps.

What it checks

Condition Assertion
Bytecode changed REINITIALIZER_VERSION must increase
Bytecode changed reinitializeVN() function must exist (N = REINITIALIZER_VERSION - 1)
Bytecode changed MAJOR, MINOR, or PATCH version must differ
Bytecode unchanged REINITIALIZER_VERSION must NOT have changed

Changes

  • ci/check-upgrade-hygiene.sh — shared validation script
  • .github/workflows/contracts-upgrade-hygiene.yml — new CI workflow
  • host-contracts/foundry.toml — add cbor_metadata = false, bytecode_hash = 'none' for deterministic bytecode
  • gateway-contracts/foundry.toml — same + OZ remappings for forge compilation

Test plan

  • Local smoke test: ACL bug scenario detected (exit 1, 2 errors)
  • Local smoke test: unchanged host-contracts pass (exit 0)
  • Local smoke test: unchanged gateway-contracts pass (exit 0)
  • CI: workflow runs on this PR
  • CI: host-contracts job correctly detects ACL bytecode change (expected fail — ACL fix from fix(host-contracts): bump ACL reinitializer version #2107 not yet merged)
  • CI: gateway-contracts job passes

Add a lightweight CI workflow that compares PR contract bytecodes against
main and fails fast if bytecode changed without proper version bumps.

When bytecode changes, the check asserts:
- REINITIALIZER_VERSION was bumped
- reinitializeVN() function exists (N = REINITIALIZER_VERSION - 1)
- Semantic version (MAJOR/MINOR/PATCH) was bumped

When bytecode is unchanged, asserts reinitializer was NOT bumped.

Covers both host-contracts and gateway-contracts via a shared script.
Runs independently of existing upgrade-tests workflows (no Docker/Anvil).

Also updates both foundry.toml files with cbor_metadata=false and
bytecode_hash='none' for deterministic path-independent bytecode, and
adds OZ remappings to gateway-contracts so forge can compile it.
@Eikix Eikix requested review from a team as code owners March 13, 2026 11:10
@cla-bot cla-bot bot added the cla-signed label Mar 13, 2026
@mergify
Copy link
Copy Markdown

mergify bot commented Mar 13, 2026

🧪 CI Insights

Here's what we observed from your CI run for 48d1154.

🟢 All jobs passed!

But CI Insights is watching 👀

@Eikix Eikix changed the title feat(ci): add contract upgrade hygiene check feat(contracts): add contract upgrade hygiene check Mar 13, 2026
Copy the PR's foundry.toml (with cbor_metadata=false and
bytecode_hash='none') to the main checkout before compiling.
Without this, main compiles with its original settings, producing
different metadata → false bytecode diffs for every contract.
Eikix added 4 commits March 13, 2026 12:39
- Replace 8 per-contract sed calls with single-pass awk extraction
  (fixes latent bug where multiple matches could silently corrupt values,
  and [0-9]* matching zero digits)
- Pre-compile both roots in parallel (forge build) so all forge inspect
  calls are cache hits — halves compilation wall time in CI
- Handle forge inspect failures gracefully (report + continue) instead
  of aborting the entire script under set -e
- Remove unnecessary chmod +x in workflow (file already has exec bit)
The previous $NF approach grabbed the last whitespace-delimited field,
which would silently pick up digits from trailing comments
(e.g. `MAJOR_VERSION = 12; // was 11` extracted 11, not 12).
Now uses val_after_eq() to extract the first integer after `=`.
forge inspect can emit compiler warnings to stderr even on success.
With 2>&1, those warnings were mixed into the bytecode string, causing
false positives when warnings differed between main and PR. Now stderr
is discarded on success (2>/dev/null) and only captured on failure for
the error message.
- Replace two near-identical jobs with a single matrix job (host-contracts,
  gateway-contracts), cutting workflow from 151 to 92 lines
- Move inline Solidity address heredocs to committed ci/stubs/ files
- Add missing PaymentBridgingAddresses.sol stub (was breaking gateway CI)
- Drop forge build pre-step: forge inspect already compiles and caches
  on first call, and forge build required ALL stubs including non-manifest
  contracts which caused failures
- Trim script from 161 to 110 lines: shorter header, flattened control flow
Copy link
Copy Markdown

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zizmor found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Eikix added 2 commits March 13, 2026 13:03
npm --prefix doesn't work with npm ci when package-lock.json is in
the target directory. Use working-directory instead. Also guard
extra-deps step with matrix.if to avoid empty subshell syntax error.
Eikix added 4 commits March 13, 2026 13:11
zizmor flags ${{ matrix.* }} in run: blocks as code injection risk.
Pass matrix values through env vars instead, which is the recommended
safe pattern for GitHub Actions.
Replace the bash script with a TypeScript version run via Bun.
Much more readable: regex instead of awk, typed data structures,
standard control flow. Adds setup-bun step to the workflow.
- Fix cumulative error count bug: use local `parseFailed` flag so a
  parse failure in contract A doesn't skip contract B
- Use try/finally for ::endgroup:: (7 sites → 1)
- Strip both // and /* */ comments before checking for reinitializeV{N}
  function (prevents false positives from commented-out signatures)
- Surface forge stderr on compilation failure instead of swallowing it
- Avoid redundant file read: extractVersions returns source alongside
  versions
- Document why foundry.toml is copied to main's checkout
@Eikix
Copy link
Copy Markdown
Contributor Author

Eikix commented Mar 13, 2026

@claude /pr-review

@claude
Copy link
Copy Markdown

claude bot commented Mar 13, 2026

Claude finished @Eikix's request.View run

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[AUTOMATED]

Code Review

Well-designed CI addition that solves a real problem (detecting bytecode changes without version bumps). The script logic is correct, error handling is thorough, and the workflow follows good security practices (pinned actions, minimal permissions, persist-credentials: false).

Two convention issues found — neither is blocking.

Summary

  • ✅ No bugs or security issues found (verified by two independent bug-hunting passes)
  • ✅ Version-checking logic correctly implements the reinitializeV{N-1} convention
  • ✅ Workflow properly isolates PR vs main compilation with shared foundry.toml
  • ✅ Stub files provide all needed address constants for compilation
  • ✅ Good use of cbor_metadata = false and bytecode_hash = 'none' for deterministic comparison
  • 📝 Two minor convention issues flagged inline

- gateway-contracts/**

check:
name: contracts-upgrade-hygiene/${{ matrix.package }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[AUTOMATED] Nit: Missing (bpr) suffix in job name.

21 out of 25 PR-triggered workflows in this repo use the (bpr) suffix for the main blocking job (e.g., host-contracts-upgrade-tests/sc-upgrade (bpr), gateway-contracts-integrity-checks/contract-integrity-checks (bpr)). This suffix likely identifies jobs configured as required status checks in branch protection rules.

Suggested change:

name: contracts-upgrade-hygiene/${{ matrix.package }} (bpr)

Confidence: 85/100


concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[AUTOMATED] Nit: Unconditional cancel-in-progress: true.

18 out of 22 workflows in this repo use the conditional pattern:

cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

Since this workflow only triggers on pull_request, the difference is purely cosmetic (it can never run on main). However, using the conditional pattern maintains consistency and is future-proof if the trigger is later expanded.

Confidence: 95/100

@claude
Copy link
Copy Markdown

claude bot commented Mar 13, 2026

Review posted successfully to PR #2108. Here's a summary:

Review Results

Verdict: COMMENT (no blocking issues)

The PR adds a well-designed CI workflow for contract upgrade hygiene checking. Two independent bug-hunting passes (opus-level) found no bugs or security issues. The version-checking logic is correct, the workflow follows good security practices (pinned actions, minimal permissions, persist-credentials: false), and the stub files are complete.

Two non-blocking convention issues were flagged inline:

  1. Missing (bpr) suffix on the check job name — 21/25 PR-triggered workflows in the repo use this suffix for branch protection rule identification
  2. Unconditional cancel-in-progress: true — 18/22 workflows use the conditional pattern ${{ github.ref != 'refs/heads/main' }} (functionally equivalent here since the workflow only triggers on PRs, but inconsistent with repo conventions)

Review URL: #2108 (review)

@Eikix
Copy link
Copy Markdown
Contributor Author

Eikix commented Mar 13, 2026

Reopening as a clean PR to clear stale zizmor findings that block mergeify.

@Eikix Eikix closed this Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant