s_currentlyFlashLoaning[token] is a single per-token bool, not a nesting-aware counter. flashloan() sets it to true at the start and unconditionally sets it back to false right before it finishes - regardless of whether some other flashloan on the same token is still open.
If a receiver's executeOperation() callback takes out a second (inner) flashloan on the same token - a legitimate composition pattern (e.g. temporarily re-borrowing the same asset to complete an intermediate step) - the inner flashloan() call finishes and correctly repays itself, but on its way out it sets s_currentlyFlashLoaning[token] = false. This silently wipes out the still-open outer loan's bookkeeping.
When the outer executeOperation() then calls repay() for its own, still-legitimately-open loan (with sufficient funds and a correct amount), repay() reverts with ThunderLoan__NotCurrentlyFlashLoaning() because the flag was already cleared by the inner call - even though the outer loan was never actually settled.
Because the entire outer transaction reverts, all state changes roll back and no funds are ever at risk - this is a function-correctness / composability defect, not a fund-safety one, matching CodeHawks' own Low-impact definition of "funds are not at risk, but function/state behaves incorrectly."
Likelihood:
Reason 1 // Requires a receiver contract to specifically implement the "take a nested flashloan of the same token from within its own callback" composition pattern - a real but specific integration pattern, not something any arbitrary direct call triggers.
Reason 2 // No attacker or malicious intent is needed - a perfectly honest receiver composing two legitimate same-token flashloan operations in one transaction hits this deterministically every time.
Impact:
Impact 1 // The entire outer transaction reverts, so no funds are ever lost or placed at risk - this is exactly CodeHawks' Low-impact category ("funds not at risk, but behavior/state incorrect").
Impact 2 // Breaks composability: integrators who reasonably assume same-token flashloans can be nested within a single transaction will have their transactions unexpectedly and unavoidably revert.
Ran with forge test --match-path "test/PoC_4.t.sol" -vv: [PASS] testNestedFlashLoanOnSameTokenBreaksOuterRepay() (gas: 311744). A NestedFlashLoanReceiver receives an outer flashloan(amountToBorrow=100e18, params=true). Inside its callback it takes an inner flashloan(50e18, params=false) on the same token, which completes and repays itself honestly. The receiver then tries to repay its own outer loan with sufficient approved funds - and this call reverts with the exact custom error ThunderLoan.ThunderLoan__NotCurrentlyFlashLoaning.selector (asserted precisely via vm.expectRevert(...), not a generic revert), proving the failure is specifically the shared-flag collision and not, e.g., insufficient balance. Full regression suite: 18/18 passing, no regressions - the bug only manifests in this specific nested-same-token composition.
Replace the shared per-token bool with a nesting-aware depth counter: increment on entry, decrement on exit, and only revert repay() when the depth is zero. This way an inner loan finishing never wipes out an outer loan's still-open state. A more thorough fix would track each flashloan's expected repayment amount in its own call-scoped context (e.g. a one-time loan id) rather than relying on any shared per-token global state at all. Add a regression test covering "receiver takes a nested same-token flashloan inside its callback, then repays the outer loan" to prevent regressions.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.