According to the protocol specification, AssetTokens should "gain interest over time depending on how often people take out flash loans." Flash loan borrowers pay fees, and these fees should increase the exchange rate, benefiting liquidity providers.
The deposit() function incorrectly calculates a fee and updates the exchange rate as if the deposit were a flash loan. This breaks the core protocol invariant that only flash loan fees should increase the exchange rate, making immediate redemptions impossible and causing protocol insolvency.
The protocol's intended flow is:
Liquidity providers deposit assets and receive AssetTokens at the current exchange rate
Users take flash loans and pay fees
These fees increase the exchange rate, making AssetTokens more valuable
Liquidity providers redeem their AssetTokens for more underlying tokens than they deposited
However, the current deposit() function implementation incorrectly treats deposits as if they generate fees:
The getCalculatedFee() function calculates a flash loan fee based on the deposited amount:
When updateExchangeRate(fee) is called in AssetToken.sol, it increases the exchange rate:
The Problem:
User deposits 10 tokens
Mints 10 AssetTokens (at 1:1 exchange rate initially)
Updates exchange rate with a calculated "fee" (increases to ~1.003:1)
Only 10 underlying tokens are in the contract
When user tries to redeem 10 AssetTokens, the calculation is: 10 * 1.003 / 1 = 10.03 tokens needed
Contract only has 10 tokens → Redemption fails!
Likelihood: High
Every single deposit triggers this vulnerability
No special conditions required
Affects all liquidity providers immediately upon deposit
The first liquidity provider to deposit will be unable to redeem their full amount
Impact: High
Protocol insolvency: Liquidity providers cannot withdraw their deposits
Violation of core protocol invariant: Interest should only come from flash loans, not deposits
Loss of funds: Early depositors effectively subsidize later depositors
Broken functionality: Basic deposit/redeem flow is non-functional
Cascading failures: As more deposits occur, the exchange rate keeps increasing, making the insolvency worse
The test testDepositAndImmidiatelyReedemSameAmount() demonstrates this vulnerability:
Example Calculation:
With flash loan fee of 0.3% (3e15) and fee precision of 1e18:
Deposit: 10e18 tokens
Calculated Fee: (10e18 * 1e18) / 1e18 * 3e15 / 1e18 = 3e16 (approximately)
Exchange Rate Update: 1e18 * (10e18 + 3e16) / 10e18 = 1.003e18
Redemption Attempt: 10e18 AssetTokens * 1.003e18 / 1e18 = 10.03e18 tokens
Available Balance: 10e18 tokens
Result: ❌ Insufficient balance → Transaction reverts
Remove the fee calculation and exchange rate update from the deposit() function. Exchange rate updates should only occur during flash loans when actual fees are collected:
The exchange rate should only be updated in the flashloan() function where actual fees are collected:
# 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.