Skip to content

test(reentrancy): ERC1155-callback reentrancy regression for both vault types - #325

Merged
thedavidmeister merged 7 commits into
mainfrom
2026-06-21-issue-316-reentrancy-tests
Jul 28, 2026
Merged

test(reentrancy): ERC1155-callback reentrancy regression for both vault types#325
thedavidmeister merged 7 commits into
mainfrom
2026-06-21-issue-316-reentrancy-tests

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds fuzz-backed reentrancy regression tests covering the ERC1155 acceptance-callback surface documented in issue #316 (rows 1/2/7 of the master reentrancy call-site table).

  • OffchainAssetReceiptVault.reentrant.t.sol: testDepositReentrancyGuardFires — a malicious receiver that re-enters vault.deposit from inside onERC1155Received is blocked by the nonReentrant guard; the outer deposit reverts with ReentrancyGuardReentrantCall. testDepositSucceedsWithNonReentrantReceiver — positive control confirming the setup is valid.
  • ERC20PriceOracleReceiptVault.reentrant.t.sol: same two tests for the oracle vault variant, exercising the base _beforeDeposit ERC20 transfer path in addition to the receipt callback.

Both tests run 256 fuzz rounds locally (all pass).

No production code changes — test-only addition.

Closes #316

Test plan

  • forge test --match-path "test/src/concrete/vault/*reentrant*" — 4/4 pass, 256 fuzz rounds each
  • CI rainix-sol/test green
  • CI copy-artifacts green (no artifact changes)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened vault protection against reentrant deposits, withdrawals, receipt transfers, confiscations, and payment-token callbacks.
    • Prevented share overspending during reentrant authorization flows.
    • Verified certification state is updated correctly before authorization checks.
    • Confirmed ETH refund callbacks can safely trigger independent deposits.
  • Tests

    • Added comprehensive regression coverage for reentrancy scenarios across asset, oracle, authorization, receipt, and payment flows.

…r both vaults

Adds fuzz-backed regression tests for the reentrancy surface documented in
issue #316 (rows 1/2/7 of the master call-site table): a malicious receiver
that re-enters vault.deposit from inside onERC1155Received is rejected by the
nonReentrant guard; the outer deposit reverts with ReentrancyGuardReentrantCall.
Positive-control test confirms the same setup deposits successfully when the
receiver is not reentrant. Covers OffchainAssetReceiptVault and
ERC20PriceOracleReceiptVault. Closes #316.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Jun 21, 2026
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c7b7b975-f1cd-4cb7-95c9-b58e1b2905b7

📥 Commits

Reviewing files that changed from the base of the PR and between 6640bb7 and 6ebc80e.

📒 Files selected for processing (1)
  • test/src/concrete/vault/OffchainAssetReceiptVaultPaymentMint.reentrant.t.sol

Walkthrough

Adds Solidity reentrancy test doubles and regression suites for ERC20, oracle, receipt callback, authorizer, certification, share transfer, ETH refund, confiscation, and payment-mint flows.

Changes

Reentrancy regression tests

Layer / File(s) Summary
Reentrant test doubles
test/concrete/*.sol
Adds configurable contracts that re-enter through ERC20 hooks, authorizer callbacks, oracle pricing, certification, share transfers, ETH refunds, and ERC1155 callbacks.
ERC20 and oracle vault regressions
test/src/concrete/vault/ERC20PriceOracleReceiptVault.reentrant.t.sol
Tests guarded receipt callbacks and asset hooks, successful normal deposits, and nested oracle deposits using distinct prices.
Offchain vault regressions
test/src/concrete/vault/OffchainAssetReceiptVault.reentrant.t.sol
Tests guarded callbacks, swapped authorizers, share-transfer balance protection, certification state observation, ETH refund reentry, and confiscation callbacks.
Payment-mint reentry regression
test/src/concrete/vault/OffchainAssetReceiptVaultPaymentMint.reentrant.t.sol
Tests payment-token reentry during mint authorization and verifies the operation reverts without increasing total supply.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 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 is related to the PR, but it only highlights ERC1155 callbacks and omits the broader reentrancy coverage added.
Linked Issues check ✅ Passed The PR adds the requested regression tests for the #316 reentrancy call sites across receipt, vault, authorizer, oracle, refund, payment, and confiscation paths.
Out of Scope Changes check ✅ Passed The changes stay within the reentrancy test and helper-contract scope and do not introduce unrelated production-code edits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-06-21-issue-316-reentrancy-tests

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.

@thedavidmeister thedavidmeister added the ai:ready AI vetter: passes review, ready for human decision label Jul 6, 2026
@thedavidmeister

thedavidmeister commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 ai:vetter
Reviewed 1d086b8: ready — adds testDepositReentrancyGuardFires + positive control for OffchainAssetReceiptVault and ERC20Price
cost 350 — reentrancy regression tests for both vault types

@thedavidmeister thedavidmeister added human:needs-work Human reviewer: needs rework and removed ai:ready AI vetter: passes review, ready for human decision labels Jul 8, 2026
@thedavidmeister

Copy link
Copy Markdown
Collaborator Author

Rework note (human): incomplete coverage of #316. The issues test-coverage checklist enumerates ~7 distinct reentry categories; this PR covers only the ERC1155 deposit-callback rows (1/2/7, per the PR body). Still uncovered: asset-ERC20 transfer hooks (rows 8/9 — the asset mock bypasses them), payment-token reentry (row 19), ETH-refund sendValue (row 11), swapped-authorizer (rows 12–15), oracle _nextId (row 20), confiscation (row 16). Exercise the remaining categories against REAL reentrant dependencies (not always-succeed mocks).

…ocks

Extends the ERC1155-callback reentrancy regression suite (rows 1/2/7) to
the remaining attacker-controllable reentry rows of the issue #316
call-site table, each exercised against a REAL reentrant dependency
rather than an always-succeed mock:

- Rows 8/9: ReentrantAsset — a genuine hooked ERC20 vault asset whose
  transferFrom (_beforeDeposit pull) and transfer (_afterWithdraw push)
  re-enter vault.deposit; both revert on the shared nonReentrant guard.
- Row 11: EthRefundReenterDepositor — re-enters deposit from receive()
  when the post-guard ETH refund lands; both deposits land independently
  under distinct receipt ids (benign, per the audit; hardening in #315).
- Rows 12/13: ReentrantAuthorizer — swapped authorizer re-enters deposit
  from the authorizeReceiptTransfer3 and _afterDeposit authorize call
  sites; both revert on the guard held by _deposit.
- Row 14: CertifyObserverAuthorizer — observes from inside the CERTIFY
  authorize callback that certifiedUntil is already written (certify is
  unguarded; effects-before-interaction is the protection).
- Row 15: ReentrantShareTransferAuthorizer — re-enters transferFrom in
  the pre-balance-write authorize window; the double-move underflows with
  ERC20InsufficientBalance, proving no overspend.
- Row 16: ReentrantConfiscator — re-enters confiscateReceipt from the
  ERC1155 acceptance callback; reverts on the nonReentrant guard.
- Row 19: payment-mint with ReentrantAsset as the payment token; the
  reentrant payment pull reverts on the guard, supply cap intact.
- Row 20: ReentrantOracle — re-enters deposit from the pre-guard _nextId
  price read; each deposit locks its own fresh price/receipt id.

Every test is mutation-validated: removing the guard under test (or
reordering the protected effect) makes exactly the claiming tests fail,
restoring makes them pass.

Co-Authored-By: Claude <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Collaborator Author

🤖 ai:producer

Producer note: rework executed — rows covered (each against a REAL reentrant dependency, all mutation-validated):

  • 8: testDepositAssetTransferFromReentrancyGuardFires (hooked asset ERC20 re-enters from _beforeDeposit pull)
  • 9: testWithdrawAssetTransferReentrancyGuardFires (hooked asset ERC20 re-enters from _afterWithdraw push)
  • 11: testDepositEthRefundReentryIsBenign (reentry via post-guard sendValue refund → fresh independent deposit, distinct ids)
  • 12: testDepositSwappedAuthorizerReceiptTransferGuardFires (swapped authorizer re-enters from authorizeReceiptTransfer3)
  • 13: testDepositSwappedAuthorizerAfterDepositGuardFires (swapped authorizer re-enters from _afterDeposit)
  • 14: testCertifyReentrantAuthorizerObservesWrittenCertification (certifiedUntil written before authorize)
  • 15: testShareTransferSwappedAuthorizerCannotOverspend (unguarded share _update window → ERC20 underflow blocks overspend)
  • 16: testConfiscateReceiptReentrantConfiscatorGuardFires (confiscator re-enters from ERC1155 acceptance callback)
  • 19: testPaymentMintReentrantPaymentTokenGuardFires (reentrant payment token during payment-mint pull; supply cap intact)
  • 20: testDepositOracleReentryIsBenignWithFreshPrice (reentrant oracle in pre-guard _nextId → fresh price per deposit)

Rows found inapplicable: none.

Mutation validation (7 mutants, each killed by exactly the claiming tests, restored after): _deposit guard removed → 7 tests fail; confiscateReceipt guard removed → row 16 fails; certifiedUntil write reordered after authorize → row 14 fails; _update authorize removed → row 15 fails; guard widened to span refund + oracle read → rows 11/20 fail; per-site authorize removal at authorizeReceiptTransfer3 / _afterDeposit → exactly rows 12 / 13(+19) fail.

Local suite: 392 passed, 1 pre-existing env-only failure (PythInterfaceProdTest needs ARBITRUM_RPC_URL, supplied in CI). All 14 reentrancy tests pass at 256 fuzz runs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 @.pre-commit-config.yaml:
- Line 1: The pre-commit configuration is an environment-specific symlink into
the Nix store and is not portable. Replace the `.pre-commit-config.yaml` symlink
with the actual repository configuration, or add setup logic that generates the
symlink locally while keeping a portable config tracked in the repository.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 35e762fd-3b6d-4d82-8aaa-7a6ef1e6c38d

📥 Commits

Reviewing files that changed from the base of the PR and between 8f68b12 and b2ccda1.

📒 Files selected for processing (11)
  • .pre-commit-config.yaml
  • test/concrete/CertifyObserverAuthorizer.sol
  • test/concrete/EthRefundReenterDepositor.sol
  • test/concrete/ReentrantAsset.sol
  • test/concrete/ReentrantAuthorizer.sol
  • test/concrete/ReentrantConfiscator.sol
  • test/concrete/ReentrantOracle.sol
  • test/concrete/ReentrantShareTransferAuthorizer.sol
  • test/src/concrete/vault/ERC20PriceOracleReceiptVault.reentrant.t.sol
  • test/src/concrete/vault/OffchainAssetReceiptVault.reentrant.t.sol
  • test/src/concrete/vault/OffchainAssetReceiptVaultPaymentMint.reentrant.t.sol

Comment thread .pre-commit-config.yaml Outdated
thedavidmeister and others added 2 commits July 10, 2026 05:39
Co-Authored-By: Claude <noreply@anthropic.com>
The dev shell rewrote the tracked symlink to a machine-local nix store
path; this diff never intended to touch it. Restored to main's target.

Co-Authored-By: Claude <noreply@anthropic.com>
@thedavidmeister thedavidmeister added ai:ready AI vetter: passes review, ready for human decision and removed human:needs-work Human reviewer: needs rework labels Jul 19, 2026
@thedavidmeister

Copy link
Copy Markdown
Collaborator Author

🤖 ai:vetter
Reviewed 6640bb7: ready — closes #316 — every reentry row the human rework note enumerated is covered against real reentrant dependencies (14 tests, 7 malicious helper contracts, both vault types + payment-mint); 7 mutants each killed by exactly the claiming tests; test-only
cost 430 — 14 reentrancy tests, real reentrant deps

…rsion

The branch predates main's rain-factory 0.1.1 -> 0.1.5 bump. Every other
file already imports through the 0.1.5 prefix; this one still used 0.1.1,
which soldeer no longer installs, so the whole suite failed to compile.
…1.5 exposes

rain-factory 0.1.5 removed `clone()` in favour of `cloneDeterministic()`
with an explicit salt. The factory is constructed fresh inside this helper,
so salt 0 cannot collide -- matching the sibling construct test in
test/src/concrete/authorize/.
@thedavidmeister

Copy link
Copy Markdown
Collaborator Author

Reviewed 6ebc80e: approve

Merge-updated and fixed two version-drift breaks before merging. Both were introduced by the branch being five weeks stale, not by the tests themselves.

What I fixed, and how I nearly missed it

The first CI run after the merge-update showed one red check, and my initial read — "every [FAIL line is the drpc quota" — was wrong in the dangerous direction. The grep came back empty because there were zero [FAIL lines at all: the suite never compiled. Absence of failing tests read as absence of failures.

The actual error, caused by this branch:

Error (6275): Source "dependencies/rain-factory-0.1.1/src/concrete/CloneFactory.sol" not found

OffchainAssetReceiptVaultPaymentMint.reentrant.t.sol:12 imported through the rain-factory-0.1.1/ prefix. Every other file in the repo uses 0.1.5, foundry.toml declares "rain-factory" = "0.1.5", and soldeer.lock carries only the 0.1.5 archive. Fixed the prefix — which surfaced the second layer:

Error (9582): Member "clone" not found or not visible after argument-dependent lookup in contract CloneFactory.

rain-factory 0.1.5 removed clone() in favour of cloneDeterministic() with an explicit salt. Switched to factory.cloneDeterministic(address(implementation), initData, bytes32(0)), matching the sibling OffchainAssetReceiptVaultPaymentMintAuthorizerV1.construct.t.sol, which does exactly that. Salt 0 is safe here because the helper constructs a fresh CloneFactory per call, so sibling clones cannot collide. The clone is fixture setup, not the behaviour under test.

Now the red really is environmental, demonstrated rather than asserted

On the final run: 0 Compiler run failed lines, 0 non-quota test failures, and the single failing test is testPythGetPriceNoOlderThan — pre-existing, unrelated to this diff, failing on vm.createSelectFork … lb.drpc.live … error code -32001: You've reached the usage limit for your current plan. Main fails the same way.

The tests

Twelve functions across three files, driven by seven purpose-built attacker contracts — and every fixture is referenced, none is scaffolding. The body claims "rows 1/2/7" of #316's table and badly understates itself; the suite reaches rows 8, 9, 11, 12, 14, 15 and 20 as well.

The best property is that the test names pin the audit's reasoning, not merely "does not revert". Row 14's mitigation is "certifiedUntil written before the call" → testCertifyReentrantAuthorizerObservesWrittenCertification. Row 15's is "ERC20 underflow prevents overspend" → testShareTransferSwappedAuthorizerCannotOverspend. If the argument stops holding, the test fails — which is what a regression test for an audit finding should do.

testDepositSucceedsWithNonReentrantReceiver is the right control: without it, a guard test passes identically whether the guard works or the callback is never reached.

Scope of Closes #316

The table is 22 rows: 16 rated none (nothing to regression-test), 4 low, 1 low (benign), 1 minor. Every non-none row has a matching test except row 18_setAuthorizer calling supportsInterface on an untrusted address, rated minor and onlyOwner-gated. Filed as a follow-up rather than held against this PR.

@thedavidmeister
thedavidmeister merged commit 242dd24 into main Jul 28, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:ready AI vetter: passes review, ready for human decision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reentrancy audit report (full call-site table) — guide for reentrancy test coverage

1 participant