Thunder Loan

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

Upgraded deposit removes fee accrual

Description

Normal behavior

The original ThunderLoan.deposit() increases the AssetToken exchange rate by calling updateExchangeRate(calculatedFee), allowing existing LPs to earn a share of deposit fees.

Specific issue

ThunderLoanUpgraded.deposit() removes the updateExchangeRate(calculatedFee) call entirely. After the upgrade, deposits no longer contribute to LP yield via the exchange rate, even though the protocol still charges an implicit fee logic in the original design.

// src/upgradedProtocol/ThunderLoanUpgraded.sol
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);
// Missing: assetToken.updateExchangeRate(calculatedFee);
// The upgraded contract silently drops deposit-side fee accrual
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}
// src/protocol/ThunderLoan.sol (original, for comparison)
function deposit(IERC20 token, uint256 amount) external ... {
// ...
uint256 calculatedFee = getCalculatedFee(token, amount);
@> assetToken.updateExchangeRate(calculatedFee); // present in original, missing in upgrade
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}

Risk

Likelihood

  • The upgrade is explicitly in scope and the new deposit() implementation is always used after upgrade.

  • Every deposit after the upgrade hits this code path.

Impact

  • Existing LPs lose deposit-side fee yield.

  • The protocol's tokenomics change silently without a documented migration plan.

  • Combined with F-03 storage collision, the upgrade makes the protocol strictly worse for LPs.

Proof of Concept

The PoC upgrades the proxy to ThunderLoanUpgraded, deposits tokens, and shows that the exchange rate does not increase, whereas the original implementation would have increased it.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import { ThunderLoan } from "src/protocol/ThunderLoan.sol";
import { ThunderLoanUpgraded } from "src/upgradedProtocol/ThunderLoanUpgraded.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract UpgradeRegressionTest {
ThunderLoan thunderLoan;
IERC20 token;
address attacker = address(0x1);
function _upgrade() internal {
ThunderLoanUpgraded impl = new ThunderLoanUpgraded();
vm.prank(thunderLoan.owner());
UUPSUpgradeable(address(thunderLoan)).upgradeTo(address(impl));
}
function test_H9_PoC_upgradedDeposit_skipsExchangeRateUpdate() public {
_upgrade();
ThunderLoanUpgraded upgraded = ThunderLoanUpgraded(address(thunderLoan));
uint256 rateBefore = thunderLoan.getAssetFromToken(token).getExchangeRate();
token.mint(attacker, 10e18);
vm.startPrank(attacker);
token.approve(address(upgraded), 10e18);
upgraded.deposit(token, 10e18);
vm.stopPrank();
uint256 rateAfter = thunderLoan.getAssetFromToken(token).getExchangeRate();
@> assertEq(rateAfter, rateBefore, "upgraded deposit no longer calls updateExchangeRate(calculatedFee)");
}
}

Recommended Mitigation

Either restore the deposit-side fee accrual in ThunderLoanUpgraded.deposit() or document the intentional removal with a migration plan that compensates LPs. If the fee is restored, also fix F-05 so that the fee is denominated in underlying tokens, not WETH.

// Restore with underlying-token-denominated fee
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);
// Accrue fee in underlying token units, not WETH
uint256 calculatedFee = (amount * s_flashLoanFee) / FEE_PRECISION;
assetToken.updateExchangeRate(calculatedFee);
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours 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!