fix(lint): detect repeated flash-loan repayment pulls inside a loop - #16628
Open
gomesalexandre wants to merge 1 commit into
Open
fix(lint): detect repeated flash-loan repayment pulls inside a loop#16628gomesalexandre wants to merge 1 commit into
gomesalexandre wants to merge 1 commit into
Conversation
`arbitrary-send-erc20` tracks flash-loan repayment licenses (one per `onFlashLoan` call) consumed by a matching `transferFrom` pull. The loop body is analyzed as a single static pass, so a repayment minted before a loop could license every pull the (single-pass) body happened to contain, even though that same pull re-executes every iteration against the one license at runtime - a real fund-drain false negative. Fix: track repayment counts as they stood at each loop's entry (`loop_repayment_floors`), and refuse to consume a repayment inside a loop unless its count exceeds that entry floor, i.e. it was freshly minted since the loop was entered. Repayments a loop never touches pass through unaffected, so code after the loop still sees them. Two related correctness issues surfaced during review and are also fixed: - `consume_repayment` now searches for an eligible matching key instead of taking the first HashMap-order match and checking the floor after, since `amount + fee` and `fee + amount` can both structurally match. - The floor mechanism is deliberately never touched by `invalidate()`, since that stack isn't snapshotted/restored around `if`/`try` branches the way `State` is - mutating it there would leak across sibling branches and turn a real vulnerability into a false negative. Provably single-iteration loop shapes (`do-while(false)`, `while` with an unconditional `break`) and a mid-loop reassign-then-remint of a repayment's own key variable are accepted, disclosed false positives - documented in comments and pinned by dedicated fixtures - traded for never missing a real repeated-pull drain. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lh6V2uPTUqauqq45BM7m5k
Contributor
✅ Changelog foundThe deterministic check will validate the changed entry. |
gomesalexandre
marked this pull request as ready for review
September 4, 2026 15:15
gomesalexandre
requested review from
0xrusowsky,
DaniPopes,
figtracer,
grandizzy,
mablr,
mattsse and
stevencartavia
as code owners
September 4, 2026 15:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
arbitrary-send-erc20tracks flash-loan repayment licenses (one peronFlashLoancall) consumed by a matchingtransferFrompull. The loop body is analyzed as a single static pass, so a repayment minted before a loop could license every pull the (single-pass) body happened to contain, even though that same pull re-executes every iteration against the one license at runtime - a real fund-drain false negative:Fix
Track repayment counts as they stood at each loop's entry (
loop_repayment_floors), and refuse to consume a repayment inside a loop unless its count exceeds that entry floor, i.e. it was freshly minted since the loop was entered. Repayments a loop never touches at all pass through the eventualmeetunchanged, so code after the loop still sees them.Two related correctness issues surfaced during review and are fixed too:
consume_repaymentnow searches for an eligible matching key instead of taking the firstHashMap-order match and checking the floor after, sinceamount + feeandfee + amountcan both structurally match.invalidate(), since that stack isn't snapshotted/restored aroundif/trybranches the wayStateis - mutating it there would leak across sibling branches and turn a real vulnerability into a false negative.Accepted trade-offs
Provably single-iteration loop shapes (
do-while(false),whilewith an unconditionalbreak) and a mid-loop reassign-then-remint of a repayment's own key variable are disclosed, accepted false positives - documented in code comments and pinned by dedicated fixtures - traded for never missing a real repeated-pull drain.Testing
badFlashLoanLoopPull- mint outside a loop, pull inside it: now correctly warns (was silent before this fix).okFlashLoanPerIterationMintAndPull- mint and pull both inside the same loop body: stays silent (legitimate per-iteration pattern, unaffected).okFlashLoanPostLoopPullStillGuarded/okFlashLoanNestedUnrelatedLoop- a pull after a loop (or nested loops) that never touch the repayment stays silently licensed by the pre-loop mint.badFlashLoanDoWhileFalseSingleIteration/badFlashLoanWhileBreakSingleIteration/badFlashLoanReassignThenRemintInLoop- the three accepted-trade-off cases above, pinned as intentional warnings.crates/lint/testdataUI suite (97 fixtures, not just this file) passes..changelog/flashloan-loop-repeated-pull.mdadded per this repo's changelog requirement.Went through four rounds of adversarial (Codex) review during development - each round's finding is reflected in the fixtures/comments above, not just fixed silently.