Thunder Loan

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

ThunderLoan reedem func() miscalculate underlying token amount to withdraw

reedem() function in ThunderLoan contrat miscalculate amount of deposit token to withdraw

Description

  • The redeem() function is intended to allow a user to burn their AssetTokens and receive the corresponding underlying ERC20 tokens.

  • reedem() function makes wrong calculation, leading to overestimate balance to withdraw, reverting the transferUnderling function() in AssetToken function

// Root cause in the codebase with @> marks to highlight the relevant section
function redeem(IERC20 token, uint256 amountOfAssetToken)
external
revertIfZero(amountOfAssetToken)
revertIfNotAllowedToken(token)
{
AssetToken assetToken = s_tokenToAssetToken[token];
uint256 exchangeRate = assetToken.getExchangeRate();
if (amountOfAssetToken == type(uint256).max) {
amountOfAssetToken = assetToken.balanceOf(msg.sender);
}
@> uint256 amountUnderlying = (amountOfAssetToken * exchangeRate) / assetToken.EXCHANGE_RATE_PRECISION();
@> assetToken.burn(msg.sender, amountOfAssetToken);
@> assetToken.transferUnderlyingTo(msg.sender, amountOfAssetToken);
emit Redeemed(msg.sender, token, amountOfAssetToken, amountUnderlying);
}

Risk

Likelihood:

Any time liquidity providers withdraw funds

Impact:

  • The AssetToken contract may attempt to transfer more tokens than it holds, causing a revert (ERC20: transfer amount exceeds balance).

Users are unable to redeem their deposits correctly.

  • The mismatch between burned AssetTokens and transferred underlying tokens breaks the deposit-redemption flow.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/ThunderLoan.sol";
import "../src/AssetToken.sol";
contract RedeemPoC is Test {
ThunderLoan thunderLoan;
AssetToken assetToken;
IERC20 tokenA;
address liquidityProvider = address(0x1234);
uint256 DEPOSIT_AMOUNT = 1e18;
function setUp() public {
// Deploy mock ERC20 and AssetToken
tokenA = new ERC20Mock("TokenA", "TKA", liquidityProvider, DEPOSIT_AMOUNT * 10);
assetToken = new AssetToken(tokenA, address(thunderLoan));
thunderLoan = new ThunderLoan();
thunderLoan.setAllowedToken(tokenA, true);
}
// This test demonstrates the vulnerability with the old redeem() function
function testRedeemFailsWithOldLogic() public {
vm.startPrank(liquidityProvider);
// Mint underlying tokens and approve ThunderLoan
tokenA.mint(liquidityProvider, DEPOSIT_AMOUNT);
tokenA.approve(address(thunderLoan), DEPOSIT_AMOUNT);
// Deposit into ThunderLoan/AssetToken
thunderLoan.deposit(tokenA, DEPOSIT_AMOUNT);
// Attempt redeem triggers ERC20 transfer revert
vm.expectRevert("ERC20: transfer amount exceeds balance");
thunderLoan.redeem(tokenA, DEPOSIT_AMOUNT);
vm.stopPrank();
}
// This test passes with the corrected redeem() function
function testRedeemWorksAfterFix() public {
vm.startPrank(liquidityProvider);
tokenA.mint(liquidityProvider, DEPOSIT_AMOUNT);
tokenA.approve(address(thunderLoan), DEPOSIT_AMOUNT);
thunderLoan.deposit(tokenA, DEPOSIT_AMOUNT);
// Redeem using corrected calculation
thunderLoan.redeem(tokenA, DEPOSIT_AMOUNT);
// Verify underlying tokens were correctly returned
uint256 balance = tokenA.balanceOf(liquidityProvider);
assertEq(balance, DEPOSIT_AMOUNT);
vm.stopPrank();
}
}

Recommended Mitigation

- remove this code
+ add this code
function redeem(IERC20 token, uint256 amountOfAssetToken)
external
revertIfZero(amountOfAssetToken)
revertIfNotAllowedToken(token)
{
AssetToken assetToken = s_tokenToAssetToken[token];
uint256 exchangeRate = assetToken.getExchangeRate();
if (amountOfAssetToken == type(uint256).max) {
amountOfAssetToken = assetToken.balanceOf(msg.sender);
}
// Correct underlying calculation using exchange rate
+ uint256 amountUnderlying = (amountOfAssetToken * assetToken.EXCHANGE_RATE_PRECISION()) / exchangeRate;
// Burn the user's AssetTokens
+ assetToken.burn(msg.sender, amountOfAssetToken);
// Transfer the correct underlying amount
+ assetToken.transferUnderlyingTo(msg.sender, amountUnderlying);
+ emit Redeemed(msg.sender, token, amountOfAssetToken, amountUnderlying);
}
Updates

Lead Judging Commences

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