ThunderLoan's exchange rate is designed to increase exclusively when flash loan fees are collected and successfully repaid by borrowers. The updateExchangeRate(fee) function in flashloan() is the only intended mechanism to reward liquidity providers over time, ensuring the rate reflects real yield generated by the protocol.
deposit() incorrectly calls updateExchangeRate() on every deposit, treating the deposited amount as if it were a flash loan fee. This means any deposit — including a malicious one with a minimal amount — inflates the exchange rate independently of any actual fee collection. Combined with the fact that updateExchangeRate() in flashloan() is called before the fee is actually received, an attacker can compound the rate increase by repeatedly calling flashloan() with a minimal totalSupply, since the rate multiplier (totalSupply + fee) / totalSupply grows larger as totalSupply decreases.
Likelihood:
Every time a usert with at least 1 token want to brake the protocol
Impact:
Increasing the exchange rate by 100x effectively renders the protocol unusable, since depositing funds to earn interest becomes economically irrational.
By depositing the minimum viable amount to avoid fee rounding to zero and iterating flash loans, an attacker can push the exchange rate to arbitrarily high values at negligible cost, making it impossible for legitimate LPs to redeem their shares as the inflated rate promises more underlying tokens than the pool physically holds.
To prove this I create a test with a malicuis contract that with only 1 token can increase the exchangerate to 100% looping flashLoan and repay
This is the contract that flashloan a minimum amount and repay in a big loop
Two separate fixes are required, one for each vulnerable function.
1. Remove updateExchangeRate from deposit()
Deposits represent neutral liquidity additions and should never affect the exchange rate. The call to updateExchangeRate must be removed entirely.
2. Move updateExchangeRate after the repayment check in flashloan()
The exchange rate should only be updated after verifying that the fee has been actually received by the protocol. Moving the call after the ending balance check ensures the rate reflects real yield.
# Summary Exchange rate for asset token is updated on deposit. This means users can deposit (which will increase exchange rate), and then immediately withdraw more underlying tokens than they deposited. # Details Per documentation: > Liquidity providers can deposit assets into ThunderLoan and be given AssetTokens in return. **These AssetTokens gain interest over time depending on how often people take out flash loans!** Asset tokens gain interest when people take out flash loans with the underlying tokens. In current version of ThunderLoan, exchange rate is also updated when user deposits underlying tokens. This does not match with documentation and will end up causing exchange rate to increase on deposit. This will allow anyone who deposits to immediately withdraw and get more tokens back than they deposited. Underlying of any asset token can be completely drained in this manner. # Filename `src/protocol/ThunderLoan.sol` # Permalinks https://github.com/Cyfrin/2023-11-Thunder-Loan/blob/8539c83865eb0d6149e4d70f37a35d9e72ac7404/src/protocol/ThunderLoan.sol#L153-L154 # Impact Users can deposit and immediately withdraw more funds. Since exchange rate is increased on deposit, they will withdraw more funds then they deposited without any flash loans being taken at all. # Recommendations It is recommended to not update exchange rate on deposits and updated it only when flash loans are taken, as per documentation. ```diff function deposit(IERC20 token, uint256 amount) external revertIfZero(amount) revertIfNotAllowedToken(token) { AssetToken assetToken = s_tokenToAssetToken[token]; uint256 exchangeRate = assetToken.getExchangeRate(); uint256 mintAmount = (amount * assetToken.EXCHANGE_RATE_PRECISION()) / exchangeRate; emit Deposit(msg.sender, token, amount); assetToken.mint(msg.sender, mintAmount); - uint256 calculatedFee = getCalculatedFee(token, amount); - assetToken.updateExchangeRate(calculatedFee); token.safeTransferFrom(msg.sender, address(assetToken), amount); } ``` # POC ```solidity function testExchangeRateUpdatedOnDeposit() public setAllowedToken { tokenA.mint(liquidityProvider, AMOUNT); tokenA.mint(user, AMOUNT); // deposit some tokenA into ThunderLoan vm.startPrank(liquidityProvider); tokenA.approve(address(thunderLoan), AMOUNT); thunderLoan.deposit(tokenA, AMOUNT); vm.stopPrank(); // another user also makes a deposit vm.startPrank(user); tokenA.approve(address(thunderLoan), AMOUNT); thunderLoan.deposit(tokenA, AMOUNT); vm.stopPrank(); AssetToken assetToken = thunderLoan.getAssetFromToken(tokenA); // after a deposit, asset token's exchange rate has aleady increased // this is only supposed to happen when users take flash loans with underlying assertGt(assetToken.getExchangeRate(), 1 * assetToken.EXCHANGE_RATE_PRECISION()); // now liquidityProvider withdraws and gets more back because exchange // rate is increased but no flash loans were taken out yet // repeatedly doing this could drain all underlying for any asset token vm.startPrank(liquidityProvider); thunderLoan.redeem(tokenA, assetToken.balanceOf(liquidityProvider)); vm.stopPrank(); assertGt(tokenA.balanceOf(liquidityProvider), AMOUNT); } ```
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.