Thunder Loan

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

exchange rate updated before token transfer in deposit() allows incorrect minting

Root + Impact

Description

  • The deposit() function should transfer tokens into the contract first, then update the exchange rate based on newly received funds.

  • The issue is that deposit() calls updateExchangeRate() BEFORE safeTransferFrom(), so the exchange rate inflates based on tokens not yet in the contract, causing subsequent depositors to receive fewer asset tokens than they deserve.

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;
assetToken.mint(msg.sender, mintAmount);
uint256 calculatedFee = getCalculatedFee(token, amount);
@> assetToken.updateExchangeRate(calculatedFee);
@> token.safeTransferFrom(msg.sender, address(assetToken), amount);
}

Risk

Likelihood:

  • This occurs on every single call to deposit() by any user at any time.

  • The impact compounds with each subsequent deposit made to the protocol.

Impact:

  • Depositors receive fewer asset tokens than entitled for their deposited amount.

  • Upon calling redeem(), liquidity providers recover less underlying than deposited, resulting in permanent loss of funds.

Proof of Concept

Add this test to test/ and run forge test --match-test testDepositInflatesRateBeforeTransfer -vv:

function testDepositInflatesRateBeforeTransfer() public {
vm.prank(thunderLoan.owner());
thunderLoan.setAllowedToken(tokenA, true);
AssetToken asset = thunderLoan.getAssetFromToken(tokenA);
address lpA = address(111);
address lpB = address(222);
uint256 amount = 100e18;
// LP A deposits
tokenA.mint(lpA, amount);
vm.startPrank(lpA);
tokenA.approve(address(thunderLoan), amount);
thunderLoan.deposit(tokenA, amount);
vm.stopPrank();
// LP B deposits same amount AFTER rate already inflated
tokenA.mint(lpB, amount);
vm.startPrank(lpB);
tokenA.approve(address(thunderLoan), amount);
thunderLoan.deposit(tokenA, amount);
vm.stopPrank();
// LP B receives fewer shares than LP A for the same deposit
uint256 lpBShares = asset.balanceOf(lpB);
assert(lpBShares < amount); // PROVES: lpB got less than 1:1
}

Test output: [PASS] — lpB receives ~99.7e18 shares instead of 100e18. On redeem, lpB recovers less than 100 tokens deposited.

Recommended Mitigation

assetToken.mint(msg.sender, mintAmount);
+ token.safeTransferFrom(msg.sender, address(assetToken), amount);
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!