Root + Impact
The deposit function updates the exchange rate before actually transferring the funds from the user, allowing attackers to exploit the inflated exchange rate during a transaction and steal value from liquidity providers.
Description
Normally, a user deposits tokens, and the protocol mints AssetTokens and records the exchange rate based on the deposited amount.
However, the specific issue is that the protocol updates the exchange rate (inflating it with theoretical fees) BEFORE executing the actual token transfer using safeTransferFrom. Because the contract believes the funds have already been deposited and increases the pool’s value prematurely, an attacker can exploit this window during a flash loan.
function deposit(IERC20 token, uint256 amount) external revertIfZero(amount) revertIfNotAllowedToken(token) {
assetToken.mint(msg.sender, mintAmount);
uint256 calculatedFee = getCalculatedFee(token, amount);
@> assetToken.updateExchangeRate(calculatedFee);
@> token.safeTransferFrom(msg.sender, address(assetToken), amount);
}
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);
}
Risk
Likelihood:
Impact:
-
High. The exchange rate is artificially inflated.
-
High. Legitimate liquidity providers lose value because the attacker can mint AssetTokens at a manipulated rate and immediately withdraw them to steal funds.
Proof of Concept
function testExchangeRateUpdatesBeforeTransfer() public {
vm.prank(user);
tokenA.approve(address(thunderLoan), 1000e18);
vm.prank(user);
thunderLoan.deposit(tokenA, 1000e18);
uint256 exchangeRateBefore = assetToken.getExchangeRate();
vm.prank(attacker);
tokenA.approve(address(thunderLoan), 500e18);
vm.prank(attacker);
thunderLoan.deposit(tokenA, 500e18);
uint256 exchangeRateAfter = assetToken.getExchangeRate();
assert(exchangeRateAfter > exchangeRateBefore);
}
Recommended Mitigation
Ensure tokens have been successfully transferred to the protocol first. Only after the receipt of funds has been confirmed should AssetTokens be minted and the exchange rate updated.
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);
+ assetToken.mint(msg.sender, mintAmount);
+ assetToken.updateExchangeRate(calculatedFee);
}