Thunder Loan

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

Exchange Rate Updated Before Fee Collection in `flashloan()`

Root + Impact

Description

  • In the `flashloan()` function, the exchange rate is updated with the calculated fee before the fee is actually collected. If the flash loan callback fails or the fee is not properly repaid, the exchange rate will still be updated, creating an accounting mismatch. The exchange rate should only be updated after the fee has been successfully collected.


    The `flashloan()` function updates the exchange rate before ensuring the fee is collected:
    ```solidity

    function flashloan(address receiverAddress, IERC20 token, uint256 amount, bytes calldata params) external {

    AssetToken assetToken = s_tokenToAssetToken[token];

    uint256 startingBalance = IERC20(token).balanceOf(address(assetToken));

    if (amount > startingBalance) {

    revert ThunderLoan__NotEnoughTokenBalance(startingBalance, amount);

    }

    if (!receiverAddress.isContract()) {

    revert ThunderLoan__CallerIsNotContract();

    }

    uint256 fee = getCalculatedFee(token, amount);

    // slither-disable-next-line reentrancy-vulnerabilities-2 reentrancy-vulnerabilities-3

    assetToken.updateExchangeRate(fee);  *// @> Exchange rate updated BEFORE fee collection*
    

    emit FlashLoan(receiverAddress, token, amount, fee, params);

    s_currentlyFlashLoaning[token] = true;

    assetToken.transferUnderlyingTo(receiverAddress, amount);

    // slither-disable-next-line unused-return reentrancy-vulnerabilities-2

    receiverAddress.functionCall(

    abi.encodeWithSignature(

    "executeOperation(address,uint256,uint256,address,bytes)",

    address(token),

    amount,

    fee,

    msg.sender,

    params

    )

    );

    uint256 endingBalance = token.balanceOf(address(assetToken));

    if (endingBalance < startingBalance + fee) { // @> Fee collection checked AFTER exchange rate update

    revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance);

    }

    s_currentlyFlashLoaning[token] = false;

    }

    ```

    The problem is that `updateExchangeRate(fee)` is called before the flash loan receiver's `executeOperation()` is called. If the receiver fails to repay properly, the transaction will revert, but if there's any edge case where the exchange rate update persists (or if there's a reentrancy issue), the accounting will be wrong.

Risk

Likelihood:

  • * This occurs on every flash loan transaction - the exchange rate is always updated before fee collection

    * If the flash loan callback fails or doesn't repay correctly, the transaction reverts, but the order of operations is still incorrect

    * The issue is present in both original and upgraded contracts

Impact:

  • * Exchange rate can be updated without corresponding fee collection, creating accounting errors

    * If combined with reentrancy or other vulnerabilities, could lead to permanent exchange rate inflation

    * Protocol's internal accounting becomes incorrect, affecting all depositors

    * Could lead to situations where the protocol thinks it has more value than it actually does

Proof of Concept

```solidity
// Scenario:
// 1. Flash loan initiated for 1000e18 tokens, fee = 3e18
// 2. Exchange rate updated: newRate = oldRate * (totalSupply + 3e18) / totalSupply
// 3. Tokens transferred to receiver
// 4. executeOperation() called
// 5. If executeOperation() fails or doesn't repay correctly:
// - Transaction reverts (good)
// - But if there's any way the exchange rate update persists, accounting is wrong
// 6. Even if transaction reverts, the order suggests the protocol assumes fee is collected before it is
```

Recommended Mitigation

Move the exchange rate update to after successful fee collection:
```diff
function flashloan(address receiverAddress, IERC20 token, uint256 amount, bytes calldata params) external {
AssetToken assetToken = s_tokenToAssetToken[token];
uint256 startingBalance = IERC20(token).balanceOf(address(assetToken));
if (amount > startingBalance) {
revert ThunderLoan__NotEnoughTokenBalance(startingBalance, amount);
}
if (!receiverAddress.isContract()) {
revert ThunderLoan__CallerIsNotContract();
}
uint256 fee = getCalculatedFee(token, amount);
- // slither-disable-next-line reentrancy-vulnerabilities-2 reentrancy-vulnerabilities-3
- assetToken.updateExchangeRate(fee);
emit FlashLoan(receiverAddress, token, amount, fee, params);
s_currentlyFlashLoaning[token] = true;
assetToken.transferUnderlyingTo(receiverAddress, amount);
// slither-disable-next-line unused-return reentrancy-vulnerabilities-2
receiverAddress.functionCall(
abi.encodeWithSignature(
"executeOperation(address,uint256,uint256,address,bytes)",
address(token),
amount,
fee,
msg.sender,
params
)
);
uint256 endingBalance = token.balanceOf(address(assetToken));
if (endingBalance < startingBalance + fee) {
revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance);
}
+ // Update exchange rate AFTER fee is confirmed collected
+ assetToken.updateExchangeRate(fee);
s_currentlyFlashLoaning[token] = false;
}
```
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 16 days 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!