Thunder Loan

AI First Flight #7
Beginner FriendlyFoundryDeFiOracle
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Erroneous exchange rate update in ThunderLoan::deposit artificially inflates AssetToken exchange rate on plain deposits

Summary

ThunderLoan::deposit erroneously calculates a flash loan fee and calls assetToken.updateExchangeRate(calculatedFee) during a standard liquidity deposit. Because no flash loan fee is actually collected during deposit(), updating the exchange rate artificially inflates AssetToken::s_exchangeRate on every deposit, breaking share-to-asset accounting conservation and leading to protocol insolvency.

Vulnerability Details / Description

In src/protocol/ThunderLoan.sol, the deposit() function mints AssetToken shares to the liquidity provider and then immediately updates the exchange rate as if a flash loan fee had been paid:

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);
​
// @audit-issue: Erroneously calculates fee and updates exchange rate on a plain deposit
uint256 calculatedFee = getCalculatedFee(token, amount);
assetToken.updateExchangeRate(calculatedFee);
​
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}

Exchange rate updates should only occur during flashloan() when actual fee revenue is added to the pool. Updating s_exchangeRate inside deposit() causes the newly minted shares (as well as existing shares) to immediately claim more underlying tokens than were actually deposited into AssetToken.

Proof of Concept

Formal state-transition verification (INV-5: Share-Price Monotonicity Guard) confirms that a standard deposit increases assetToken.getExchangeRate() without any flash-loan fee entering the reserve:

You can also verify this in Foundry by adding the following test to test/unit/ThunderLoanTest.t.sol:

function testDepositArtificiallyInflatesExchangeRate() public setAllowedToken {
uint256 amountToDeposit = 100e18;
tokenA.mint(liquidityProvider, amountToDeposit);
​
uint256 rateBefore = assetToken.getExchangeRate();
​
vm.startPrank(liquidityProvider);
tokenA.approve(address(thunderLoan), amountToDeposit);
thunderLoan.deposit(tokenA, amountToDeposit);
vm.stopPrank();
​
uint256 rateAfter = assetToken.getExchangeRate();
// Rate increased even though zero flash loans occurred!
assertGt(rateAfter, rateBefore);
}

Impact/Risk

Tools Used

Manual Review, Foundry, Neuro-Symbolic Formal Verification (Z3).

Recommended Mitigation

Remove the fee calculation and updateExchangeRate call from ThunderLoan::deposit:

--- a/src/protocol/ThunderLoan.sol
+++ b/src/protocol/ThunderLoan.sol
@@ -142,8 +142,6 @@ contract ThunderLoan is Initializable, OwnableUpgradeable, UUPSUpgradeable, Orac
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);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!