updateExchangeRate adds fee, denominated in the underlying token, to totalSupply(), denominated in shares. The two are only interchangeable while the exchange rate is exactly 1.0; once it moves, the multiplier is computed against the wrong base.
The correct rate is (underlyingHeld + fee) * PRECISION / totalSupply(). The implementation instead multiplies the previous rate by a share-based ratio, so each fee compounds on the already-raised rate rather than tracking the underlying actually held. The two agree only on the first fee, then diverge permanently.
Likelihood: High
Every flash loan calls updateExchangeRate, so the divergence accumulates during entirely normal operation. No attacker, no special conditions, and users cannot avoid it.
Impact: High
The exchange rate overstates what each share is worth, so the protocol promises more underlying than it holds. The gap grows with every loan and is unrecoverable: depositors who redeem early are paid out of later depositors' principal, and the last holders cannot withdraw at all.
Sixty ordinary, fully repaid flash loans against a 1000e18 pool, with every fee honestly paid in.
Result:
A second test confirms the consequence: the sole liquidity provider's redeem(type(uint256).max) reverts, so their own position is no longer withdrawable.
Derive the rate from the underlying actually held rather than compounding the previous rate against share supply. Fees are paid in the underlying, so they belong in the numerator alongside the existing underlying, not added to a share count:
This makes the rate an exact statement of underlying-per-share at all times, so redemption can always be honoured. The mismatch is invisible while the rate sits at 1.0, which is why it survives the existing test suite.
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.