Skip to content

fix(RingHTLC): bounded withdraw + authenticated hop membership (audit HL-05, HL-06)#1

Merged
BarisSozen merged 2 commits into
masterfrom
fix/ring-htlc-hardening
Jun 10, 2026
Merged

fix(RingHTLC): bounded withdraw + authenticated hop membership (audit HL-05, HL-06)#1
BarisSozen merged 2 commits into
masterfrom
fix/ring-htlc-hardening

Conversation

@BarisSozen

@BarisSozen BarisSozen commented Jun 10, 2026

Copy link
Copy Markdown
Member

Audit PR-11 — RingHTLC hardening (HL-05 + HL-06) + CI

Wave-3 remediation from the 2026-06 multi-repo security audit (main repo AUDIT/FIX-PLAN.md §PR-11). This repo is standalone with no deployments (Phase 0 verified it is not wired into the main settlement path), so the breaking ABI change ships directly.

HL-05 (MEDIUM) — withdraw had no upper timelock bound

After a hop's expiry, withdraw and refund were simultaneously valid — a first-tx-wins race. In a staggered-timelock ring, a late preimage reveal could withdraw a hop whose sender already refunded an adjacent hop, leaving the ring part-WITHDRAWN/part-REFUNDED (atomicity break). withdraw now requires block.timestamp < hop.timelock, mirroring the core EVM (HashedTimelockEther.sol:130) and Sui HTLCs — withdraw and refund are temporally exclusive, and the staggered gaps are exactly the post-reveal claim windows.

HL-06 (MEDIUM) — unauthenticated ring membership griefed isRingSettled

ringId was a caller-chosen bytes32: anyone could append a dust-funded junk hop to a victim's ring; isRingSettled iterated every pushed hop, so the result was false forever. Two-part fix:

  1. Hop binding enforced at creation: createHopETH/ERC20 take hopIndex and require hopId == keccak256(ringId, hopIndex, msg.sender, receiver)computeHopId was documented but never enforced. A hop slot is now unforgeable (sender is part of the binding, so an outsider cannot occupy a participant's expected hopId).
  2. isRingSettled(ringId, expectedHopIds[]) replaces the unbounded form: the coordinator checks its own deterministic hop set; junk hops under the same ringId are never consulted, foreign-ring hops fail the ringId match, empty sets are never settled.

Also documents the keccak256-vs-sha256 incompatibility with the core HashLock HTLCs in the contract header (audit informational note — do not compose without a hash bridge).

Tests + CI

  • Forge audit suite flipped from unsafe-demonstration to safe-behavior pins (the audit-phase tests passed by exhibiting the bugs): expired withdraw rejected + refund path intact; pre-expiry withdraw works; forged hopId rejected; junk hop cannot grief the expected-set check; foreign/unsettled/empty sets rejected; preimage-soundness fuzz + no-double-resolve retained. 8/8.
  • Hardhat suite updated to the new signatures: 21/21.
  • New CI (.github/workflows/ci.yml): Hardhat compile+test and Foundry audit-suite jobs — first CI this repo has had (audit follow-up item).
  • foundry.toml + the forge audit suite are now tracked (were local-only from the audit phase); lib/+out-forge/ gitignored, forge-std installed in CI.

Off-chain coordinator

src/coordinator.ts already computes binding-correct hop IDs and tracks status off-chain only — no change required.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Strengthened security validation for atomic ring settlements with enhanced hop verification and temporal exclusivity checks.
  • Testing

    • Added automated CI/CD pipeline executing comprehensive tests on all pull requests and master branch pushes.
    • Expanded test coverage with regression and invariant test suites.
  • Infrastructure

    • Updated build configuration and added Foundry toolchain support.

… HL-05, HL-06)

HL-05: withdraw had no upper timelock bound — after a hop's expiry both
withdraw and refund were simultaneously valid (first-tx-wins race). A late
preimage reveal could leave a ring part-WITHDRAWN/part-REFUNDED, breaking
the atomicity the staggered timelocks exist to provide. withdraw now
requires block.timestamp < hop.timelock (mirrors the core EVM/Sui HTLCs),
making withdraw and refund temporally exclusive.

HL-06: ringId was an unauthenticated bytes32 — anyone could append a
self-funded junk hop to a victim's ring and force isRingSettled false
forever (coordinator griefed indefinitely at dust cost). Two-part fix:
- createHopETH/createHopERC20 take hopIndex and enforce
  hopId == keccak256(ringId, hopIndex, msg.sender, receiver) — hop slots
  are unforgeable (computeHopId was documented but never enforced)
- isRingSettled(ringId, expectedHopIds[]) replaces the unbounded form:
  the coordinator checks ITS deterministic hop set; junk hops under the
  same ringId are never consulted, and a foreign/forged hop fails the
  ringId-match check

Also documents the keccak256-vs-sha256 incompatibility with the core
HashLock HTLCs in the contract header (audit informational note).

Audit suite flipped from demonstrating the unsafe behavior to pinning the
safe behavior: forge 8/8 (incl. expired-withdraw rejection + refund-path
intact, forged-hopId rejection, junk-hop grief immunity, foreign-hop and
empty-set rejection). Hardhat suite updated to the new signatures: 21/21.

Breaking ABI change — standalone repo, no deployments (Phase 0 verified
not wired into the main settlement path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@BarisSozen, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 54 minutes and 3 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 79909825-f695-469c-a2bd-ce0eec203418

📥 Commits

Reviewing files that changed from the base of the PR and between 96d2814 and 5fbabbe.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • README.md
📝 Walkthrough

Walkthrough

This PR strengthens atomic ring settlement security by introducing deterministic hop ID binding, enforcing temporal settlement windows, and requiring caller-supplied expected hop sets for settlement verification. The contract now rejects forged hop IDs at creation, prevents withdrawal after timelock expiry, and prevents junk-hop griefing of settlement detection. Comprehensive Forge audit tests and TypeScript test migration validate all changes.

Changes

Ring Settlement Security & Verification

Layer / File(s) Summary
Hop ID Deterministic Binding & Validation
contracts/RingHTLC.sol
Contract documents hash-function mismatch risk and requires hopIndex parameter at creation; both createHopETH and createHopERC20 now validate hop IDs as keccak256(ringId, hopIndex, sender, receiver) to prevent forgery.
Withdraw Timelock Boundary Check
contracts/RingHTLC.sol
withdraw now explicitly checks block.timestamp < hop.timelock, making withdrawal impossible after timelock expiry while refund remains available, ensuring temporal exclusivity.
Settlement Query with Expected Hop Set
contracts/RingHTLC.sol
isRingSettled refactored to require caller-supplied expected hop ID array instead of iterating internally stored hops, preventing junk-hop griefing of settlement detection.
Foundry Audit Test Suite
test-forge/RingHTLC.audit.t.sol
Comprehensive regression tests validate preimage correctness, double-resolve prevention, timelock boundaries, hop ID binding enforcement at creation, and settlement validation against expected hop sets.
TypeScript Test Rewiring
test/RingHTLC.test.ts
Existing test suite systematically updated to pass hopIndex argument to hop creation and supply expected hop ID arrays to isRingSettled across all scenarios: 3-party rings, ERC20, multi-party, timelocks, refunds, and failure cases.
CI & Build Configuration
.github/workflows/ci.yml, foundry.toml, .gitignore
GitHub Actions workflow runs Hardhat and Foundry tests in parallel on master pushes and PRs; Foundry configuration defines source/test/output paths and invariant testing parameters; build artifacts excluded from version control.

🎯 3 (Moderate) | ⏱️ ~25 minutes

A rabbit hops through rings with care,
Each hopIndex bound with flair,
Timelocks guarded, forgeries bare,
Settlement waits for the expected pair—
Now tests audit all the air! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the two main security fixes (bounded withdraw and authenticated hop membership) with audit references (HL-05, HL-06), directly matching the PR's core objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ring-htlc-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@BarisSozen

Copy link
Copy Markdown
Member Author

Independent review (Claude /ultrareview)

Diff scope. Audit PR-11 (~320 LOC): HL-05 — withdraw gains the upper timelock bound (block.timestamp < hop.timelock), making withdraw/refund temporally exclusive; HL-06 — createHopETH/ERC20 gain hopIndex and enforce hopId == keccak256(ringId, hopIndex, msg.sender, receiver), and isRingSettled becomes (ringId, expectedHopIds[]); audit forge suite flipped from unsafe-demonstration to safe-behavior pins (8/8); Hardhat suite updated (21/21); first CI workflow added; foundry config + forge tests now tracked.

Findings

  1. MEDIUM.github/workflows/ci.yml (foundry job): the contract imports OpenZeppelin via the node_modules/@openzeppelin/ remapping in foundry.toml, but the foundry job only runs forge install forge-std — no npm ci — so forge test will fail to compile in CI on the missing OZ sources. Add actions/setup-node + npm ci before forge test (the local runs pass only because node_modules exists).

  2. NITREADME.md:100,113 documents the pre-fix API (createHopETH without hopIndex, isRingSettled(ringId) without the expected set). Stale after this PR; update the snippets.

  3. NIT (document, no change)isRingSettled(ringId, expectedHopIds) is only sound when the verifier derives expectedHopIds from the ring definition itself (participants + indices via computeHopId). A consumer that accepts a hop list from an untrusted party could be shown a 1-element "ring". Worth one sentence in the natspec/README usage note.

Verified safe (checked explicitly)

  • Boundary semantics: withdraw requires < timelock, refund requires >= timelock — no overlap, no dead zone at t == timelock; matches the core EVM HTLC exactly.
  • CEI + reentrancy: status flips to WITHDRAWN/REFUNDED before the external call, nonReentrant retained — unchanged safe pattern.
  • Binding completeness: an outsider cannot occupy a participant's expected hopId (msg.sender is inside the hash); junk hops under a victim ringId remain creatable with the attacker's own binding but are never consulted by the expected-set check, and a WITHDRAWN hop from another ring fails the hop.ringId != ringId match — both pinned by tests.
  • encodePacked safety: (bytes32, uint8, address, address) — all fixed-width, no ambiguous packing; on-chain check mirrors computeHopId and the off-chain coordinator (solidityPackedKeccak256(["bytes32","uint8","address","address"]), already index-aware — no coordinator change needed).
  • uint8 hopIndex caps rings at 256 hops — far above any plausible ring size; coordinator enforces ≥2 participants only.
  • ABI break is safe here: standalone repo, no deployments (Phase 0 verified not wired into the main settlement path).
  • Pre-existing, unchanged: fee-on-transfer ERC20s would under-fund a hop (recorded amount vs received) — out of this PR's findings scope; note for any future productionization alongside the keccak256-vs-sha256 incompatibility now documented in the header.

Risk assessment

  • Compile: forge + hardhat compile clean locally; CI as written will fail the foundry job (finding fix(RingHTLC): bounded withdraw + authenticated hop membership (audit HL-05, HL-06) #1).
  • Security: strictly tightening; no new external-call surface; no storage-layout concern (no proxy).
  • Tests: forge 8/8 (incl. fuzz preimage soundness), hardhat 21/21 — both run locally; the flipped tests now FAIL against the pre-fix contract, which is the property the audit wanted.

Verdict

APPROVE WITH SUGGESTIONS — fix the CI foundry job (#1) before merge so the new gate is green on day one; #2/#3 are doc touch-ups.

… API update

Review #1: the foundry CI job compiles the contract through the
node_modules OpenZeppelin remapping but never ran npm ci — would fail on
day one. Review #2: README still showed the pre-HL-06 signatures; updated
with the hopIndex param, the expected-set isRingSettled, and the note that
expectedHopIds must be derived via computeHopId, never accepted from an
untrusted party (review #3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BarisSozen

Copy link
Copy Markdown
Member Author

Review follow-up: all three findings addressed in
5fbabbe — foundry CI job now runs npm ci before forge test (OZ remapping), README updated to the new signatures with the expected-set derivation warning. Forge 8/8 + Hardhat 21/21 re-verified locally.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contracts/RingHTLC.sol (1)

4-6: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Foundry CI fails to resolve OpenZeppelin imports.

The pipeline errors show Forge cannot find @openzeppelin/ paths. Hardhat resolves these via node_modules/, but Foundry needs explicit remappings.

🔧 Proposed fix: add remappings to foundry.toml
[profile.default]
# ... existing config ...
remappings = [
    "`@openzeppelin/`=node_modules/@openzeppelin/",
]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/RingHTLC.sol` around lines 4 - 6, Foundry cannot resolve the
`@openzeppelin` imports used in contracts/RingHTLC.sol (the IERC20, SafeERC20,
ReentrancyGuard imports); add an explicit remapping for `@openzeppelin/` in
foundry.toml (e.g., add "`@openzeppelin/`=node_modules/@openzeppelin/" to the
remappings array under the default profile or top-level remappings) so Forge can
locate those modules, then rerun forge build to verify resolution.

Source: Pipeline failures

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Line 13: Update the actions/checkout@v4 steps to disable persisting the GitHub
token by adding the input persist-credentials: false to each checkout step (both
occurrences of actions/checkout@v4), so the token is not stored in .git/config
and cannot be leaked to subsequent steps or artifacts.
- Around line 13-14: Replace mutable action tags with immutable commit SHAs: for
each usage like actions/checkout@v4 and actions/setup-node@v4 (and the other
occurrences on the file at the two locations referenced), lookup the exact
commit SHA for the desired release in the upstream repos (actions/checkout and
actions/setup-node) and replace the tag reference (e.g., actions/checkout@v4)
with the full commit SHA (e.g., actions/checkout@sha) to pin the workflow to
that immutable commit; do this for both occurrences referenced (lines with
actions/checkout and actions/setup-node and the second pair at 26-27) and verify
the SHAs correspond to the intended release before committing.
- Line 28: The workflow currently runs the unfixed command "forge install
foundry-rs/forge-std --no-git" which pulls the latest default branch; change
that command to pin a specific version by installing a tag or commit, e.g.
replace it with "forge install foundry-rs/forge-std@<tag-or-commit> --no-git"
(use a released tag or exact commit SHA), or alternatively vendor/lock the
dependency by committing lib/forge-std as a git submodule and remove the install
step entirely; update the workflow step that contains "forge install
foundry-rs/forge-std --no-git" accordingly.
- Around line 1-7: The workflow currently uses the default GITHUB_TOKEN
permissions; add an explicit top-level permissions block to scope the token to
read-only (e.g., add a top-level "permissions:" entry with "contents: read" or
other minimal scopes required) so the CI workflow named "CI" only has read
access for push/pull_request triggers; update the YAML near the top (after the
workflow name) to include the permissions mapping.

---

Outside diff comments:
In `@contracts/RingHTLC.sol`:
- Around line 4-6: Foundry cannot resolve the `@openzeppelin` imports used in
contracts/RingHTLC.sol (the IERC20, SafeERC20, ReentrancyGuard imports); add an
explicit remapping for `@openzeppelin/` in foundry.toml (e.g., add
"`@openzeppelin/`=node_modules/@openzeppelin/" to the remappings array under the
default profile or top-level remappings) so Forge can locate those modules, then
rerun forge build to verify resolution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f74d795e-de1e-41e7-884f-a5492b64972e

📥 Commits

Reviewing files that changed from the base of the PR and between 30d607c and 96d2814.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • .gitignore
  • contracts/RingHTLC.sol
  • foundry.toml
  • test-forge/RingHTLC.audit.t.sol
  • test/RingHTLC.test.ts

Comment thread .github/workflows/ci.yml
Comment on lines +1 to +7
name: CI

on:
push:
branches: [master]
pull_request:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Explicitly scope permissions to follow least-privilege principle.

The workflow inherits default broad GITHUB_TOKEN permissions. Neither job requires write access, so permissions should be scoped to read-only.

🔐 Proposed fix
 name: CI

 on:
   push:
     branches: [master]
   pull_request:
+
+permissions:
+  contents: read
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: CI
on:
push:
branches: [master]
pull_request:
name: CI
on:
push:
branches: [master]
pull_request:
permissions:
contents: read
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 1-30: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 1 - 7, The workflow currently uses the
default GITHUB_TOKEN permissions; add an explicit top-level permissions block to
scope the token to read-only (e.g., add a top-level "permissions:" entry with
"contents: read" or other minimal scopes required) so the CI workflow named "CI"
only has read access for push/pull_request triggers; update the YAML near the
top (after the workflow name) to include the permissions mapping.

Source: Linters/SAST tools

Comment thread .github/workflows/ci.yml
name: Hardhat tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false to prevent GitHub token leakage.

By default, actions/checkout persists the GitHub token in .git/config, which could leak through artifacts or be accessed by subsequent steps. Since neither job requires git operations after checkout, the token should not be persisted.

🛡️ Proposed fix
       - uses: actions/checkout@v4
+        with:
+          persist-credentials: false

Apply to both checkout steps (lines 13 and 26).

Also applies to: 26-26

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 13-13: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 13-13: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 13, Update the actions/checkout@v4 steps to
disable persisting the GitHub token by adding the input persist-credentials:
false to each checkout step (both occurrences of actions/checkout@v4), so the
token is not stored in .git/config and cannot be leaked to subsequent steps or
artifacts.

Source: Linters/SAST tools

Comment thread .github/workflows/ci.yml
Comment on lines +13 to +14
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin actions to immutable commit SHAs to prevent supply-chain attacks.

Tag references (@v4, @v1) are mutable and vulnerable to tag-rewriting attacks. If an action repository is compromised, malicious code can be injected into existing tags.

🔒 Proposed fix: pin to commit SHAs
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
-      - uses: actions/setup-node@v4
+      - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
        with:
          node-version: 20
          cache: npm
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
-      - uses: foundry-rs/foundry-toolchain@v1
+      - uses: foundry-rs/foundry-toolchain@8e967ec25b47f78a3f3d5a3f967382d391399497 # v1.2.0

Also applies to: 26-27

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 13-13: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 13-13: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 14-14: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 13 - 14, Replace mutable action tags
with immutable commit SHAs: for each usage like actions/checkout@v4 and
actions/setup-node@v4 (and the other occurrences on the file at the two
locations referenced), lookup the exact commit SHA for the desired release in
the upstream repos (actions/checkout and actions/setup-node) and replace the tag
reference (e.g., actions/checkout@v4) with the full commit SHA (e.g.,
actions/checkout@sha) to pin the workflow to that immutable commit; do this for
both occurrences referenced (lines with actions/checkout and actions/setup-node
and the second pair at 26-27) and verify the SHAs correspond to the intended
release before committing.

Source: Linters/SAST tools

Comment thread .github/workflows/ci.yml
steps:
- uses: actions/checkout@v4
- uses: foundry-rs/foundry-toolchain@v1
- run: forge install foundry-rs/forge-std --no-git

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin forge-std to a specific version to ensure deterministic builds.

Installing foundry-rs/forge-std without a version specifier pulls the latest code from the default branch at build time. This leads to non-deterministic CI runs—different builds may test against different library versions, causing flaky tests or missing regressions.

📌 Proposed fix: pin to a version tag or commit
-      - run: forge install foundry-rs/forge-std --no-git
+      - run: forge install foundry-rs/forge-std@v1.9.4 --no-git

Alternatively, commit lib/forge-std as a git submodule in the repository to lock the exact version, then remove this install step entirely.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- run: forge install foundry-rs/forge-std --no-git
- run: forge install foundry-rs/forge-std@v1.9.4 --no-git
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 28, The workflow currently runs the unfixed
command "forge install foundry-rs/forge-std --no-git" which pulls the latest
default branch; change that command to pin a specific version by installing a
tag or commit, e.g. replace it with "forge install
foundry-rs/forge-std@<tag-or-commit> --no-git" (use a released tag or exact
commit SHA), or alternatively vendor/lock the dependency by committing
lib/forge-std as a git submodule and remove the install step entirely; update
the workflow step that contains "forge install foundry-rs/forge-std --no-git"
accordingly.

@BarisSozen BarisSozen merged commit 6dd9de1 into master Jun 10, 2026
1 of 3 checks passed
@BarisSozen BarisSozen deleted the fix/ring-htlc-hardening branch June 10, 2026 21:04
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.

1 participant