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

Exchange rate is calculated incorrectly, so depositors won't get the right amount of tokens/asset tokens upon deposit and redemption

Summary

The exchange rate function calculates the new exchange rate as the (s_exchangeRate * ((TotalSupply() (of asset tokens or shares in the vault) + fee) / TotalSupply())). This is not the correct way to calculate the exchange rate. The reason it is not correct to just add the fee to TotalSupply() is because TotalSupply() is in terms of asset tokens or shares in the vault, and the fee is in terms of underlying tokens, so you can't just add them.

The shares received by a depositor should be calculated as (assets deposited * TotalSupply)/(TotalAssets + 1). (TotalAssets is the amount of underlying token in the vault. +1 is to avoid dividing by 0 the first deposit.) Then the exchange rate = shares received / assets deposited.

But, for purposes of deposits and redemptions, you don't even need to calculate an exchange rate - as further explained below, it is more straightforward (and less can go wrong) to just calculate the asset token shares to be received upon a deposit or the underlying tokens to be received upon a redemption without tracking the exchange rate. You can do this based on just the amount of tokens/asset tokens of the deposit/redemptions and the amount of underlying tokens and asset tokens in AssetToken.sol.

If you want to calculate an exchange rate for purposes of invariant testing, you can still do that - you just don't need it for deposits and redemptions.

Vulnerability Details

function updateExchangeRate(uint256 fee) external onlyThunderLoan {
uint256 newExchangeRate = (s_exchangeRate *
(totalSupply() + fee)) /
totalSupply();

Impact

The exchange rate is not calculated correctly. People will not receive the right amount of underlying tokens and asset tokens when they deposit and redeem.

Tools Used

Manual review

Recommendations

You don't need to calculate the exchange rate at all for purposes of deposits and redemptions. Instead you can do the following to just calculate the amount of shares to be received when a depositor puts tokens in and the amount of tokens to get back when the depositor redeems. Make the following changes:

Add a TotalAssets function to AssetToken.sol:

function totalAssets() public view returns(uint256) {
return i_underlying.balanceOf(address(this));
}

Add a function for shares to be received by depositors to AssetToken.sol - this will be used in the deposit function instead of the exchange rate:

function sharesToBeReceived(uint256 depositAmount) external onlyThunderLoan returns(uint256) {
return ((depositAmount * TotalSupply())/TotalAssets() + 1);
}

Add a function for underlying tokens to be returned upon redemption to AssetToken.sol - this will be used in redeem instead of the exchange rate:

function tokensToBeReceived(uint256 assetTokenAmount) external onlyThunderLoan returns(uint256) {
return ((assetTokenAmount * TotalAssets)/TotalSupply());
}

Then, in ThunderLoan.sol, you would change the deposit and redeem functions to remove the exchange rate function and call assetToken.sharesToBeReceived(amount) and assetToken.tokensToBeReceived(amountofAssetToken) instead:

function deposit(IERC20 token, uint256 amount) external revertIfZero(amount) revertIfNotAllowedToken(token) {
AssetToken assetToken = s_tokenToAssetToken[token];
uint256 mintAmount = assetToken.sharesToBeReceived(amount);
emit Deposit(msg.sender, token, amount);
assetToken.mint(msg.sender, mintAmount);
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}
function redeem(
IERC20 token,
uint256 amountOfAssetToken
)
external
revertIfZero(amountOfAssetToken)
revertIfNotAllowedToken(token)
{
AssetToken assetToken = s_tokenToAssetToken[token];
if (amountOfAssetToken == type(uint256).max) {
amountOfAssetToken = assetToken.balanceOf(msg.sender);
}
uint256 amountUnderlying = assetToken.tokensToBeReceived(amountOfAssetToken);
emit Redeemed(msg.sender, token, amountOfAssetToken, amountUnderlying);
assetToken.burn(msg.sender, amountOfAssetToken);
assetToken.transferUnderlyingTo(msg.sender, amountUnderlying);
}

You also don't need to call updateExchangeRate() in the flashloan function. The reason is that the flashloan function (via calling repay) sends the fees, consisting of underlying tokens, to AssetToken.sol. Therefore, they become part of TotalAssets() and will be picked up by the sharesToBeReceived() and tokensToBeReceived() calculations by virtue of being in AssetToken.sol.

Updates

Lead Judging Commences

0xnevi Lead Judge
over 1 year ago
0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Other
happyformerlawyer Submitter
over 1 year ago
0xnevi Lead Judge
over 1 year ago
0xnevi Lead Judge
over 1 year ago
0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

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