OracleUpgradeable.getPriceInWeth() reads the connected TSwap pool's live spot price (getPriceOfOnePoolTokenInWeth()) and returns it directly - no TWAP, no multi-source cross-check, no deviation threshold, and no freshness/liquidity check.
ThunderLoan.getCalculatedFee() fully trusts this instantaneous value to compute the flash-loan fee: fee = amount * getPriceInWeth(token) * flashLoanFeeRate / precision^2. TSwap is a constant-product (x*y=k) AMM, so its spot price is just the pool's reserve ratio - anyone can move it arbitrarily with a single large swap, then reverse it with an opposite swap afterward (paying only the pool's own swap fee/slippage). Both the manipulating swap and the flashloan() call can be packed into one atomic transaction, and the manipulation capital itself can be borrowed via a flash loan elsewhere, so no real capital is required.
Because the fee is strictly linear in this manipulable price with no bounds, an attacker can crash the price before borrowing to pay a near-zero fee (directly stealing LP fee revenue), or pump it to grief/deny honest borrowers with an inflated fee requirement.
src/upgradedProtocol/ThunderLoanUpgraded.sol reuses the identical getCalculatedFee/getPriceInWeth logic unchanged, so the upcoming upgrade carries the same defect forward.
This is an independent root cause from the separate deposit()-triggers-updateExchangeRate() cluster: fixing one does not fix the other, and both currently coexist in the same deposit()/flashloan() code paths.
Likelihood:
Reason 1 // Requires only a single large swap against the external pool (and an optional reverse swap after), which needs no special permission or timing - any address can do this atomically alongside the flashloan call.
Reason 2 // The manipulation capital itself can be sourced via a flash loan from elsewhere, so the attacker needs no meaningful capital of their own - only gas and the target pool's swap fee/slippage.
Impact:
Impact 1 // An attacker can push the fee to near-zero and capture flash loans while paying a fraction of a cent on the dollar, directly at the expense of LPs who are owed the fee.
Impact 2 // The same primitive can push the fee arbitrarily high, denying honest borrowers who fund only the documented 0.3% rate.
Ran with forge test --match-path "test/PoC_5.t.sol" -vv: both tests pass. test_FeeIsFullyLinearInManipulablePrice shows getCalculatedFee() returning exactly 3e17 at the true price (1e18), exactly 3e14 (1000x smaller) after crashing the price 1000x, and exactly 3e20 (1000x larger) after pumping it 1000x - proving the fee is strictly linear and unbounded. test_AttackerBorrowsForNearZeroFee_AtomicManipulation runs the full attack in one external call: crash the price 1000x, call flashloan(100e18) and repay at the now near-zero fee, then restore the price - all atomically. The attacker actually pays only 3e14 instead of the honest 3e17 (99.9% less), the pool price is fully restored by the end of the call (no trace left), and the LP's exchange-rate gain is confirmed ~1000x smaller than what an honest fee would have produced (assertApproxEqRel within 1%). Log output: fee at true price (1e18): 300000000000000000, fee at crashed price (1e15): 300000000000000, fee at pumped price (1e21): 300000000000000000000, fee attacker actually paid: 300000000000000. Full regression suite passing, no regressions.
Replace the raw spot-price read with a time-weighted average price (TWAP) over a meaningful observation window, so a single atomic transaction cannot move the price used for fee calculation.
Cross-check against a second, independent price source (e.g. a Chainlink feed) and pause or fall back to a conservative default fee when the two sources diverge beyond a configured threshold.
Add freshness/minimum-liquidity checks so a newly created or thinly-liquid pool cannot be used as the sole price reference immediately after being manipulated.
As a defense-in-depth measure, clamp getCalculatedFee()'s output to a sane min/max band around the configured s_flashLoanFee rate, so even a compromised price feed cannot push the effective fee to near-zero or to an unreasonable multiple.
Apply this fix to both src/protocol/OracleUpgradeable.sol and src/upgradedProtocol/ThunderLoanUpgraded.sol, since the upgraded contract currently reuses the identical vulnerable logic.
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.