Summary
Whenever a user repays their borrowed funds the balance increase of the debt is increamented twice by the index. Thus this is calculated wrongly and will lead to a wrong account of the debt amount's interest.
Vulnerability Details
when a user repays
function _repay(uint256 amount, address onBehalfOf) internal {
if (amount == 0) revert InvalidAmount();
if (onBehalfOf == address(0)) revert AddressCannotBeZero();
UserData storage user = userData[onBehalfOf];
ReserveLibrary.updateReserveState(reserve, rateData);
uint256 userDebt = IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf);
uint256 userScaledDebt = userDebt.rayDiv(reserve.usageIndex);
uint256 actualRepayAmount = amount > userScaledDebt ? userScaledDebt : amount;
uint256 scaledAmount = actualRepayAmount.rayDiv(reserve.usageIndex);
@audit>> (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
reserve.totalUsage = newTotalSupply;
user.scaledDebtBalance -= amountBurned;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit Repay(msg.sender, onBehalfOf, actualRepayAmount);
}
The debttoken gets the user's debt balance
function burn(
address from,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
if (from == address(0)) revert InvalidAddress();
if (amount == 0) {
return (0, totalSupply(), 0, 0);
}
@audit>>>> uint256 userBalance = balanceOf(from);
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
@audit>>>> balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
amount = amount;
}
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
_burn(from, amount.toUint128());
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}
The actually debt is always calculated by multiplying scaled balance with index
see below
* @notice Returns the scaled debt balance of the user
* @param account The address of the user
* @return The user's debt balance (scaled by the usage index)
*/
1. function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}
The balance of function returns the actual debt that we are owing and not the scaled balance/ debt.
When we try to obtain the balance increase we multiply the actual debt obtained by raymul index again which will now give us a completely wrong value
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
@audit>> balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
amount = amount;
}
This will return a wrong amount to mint causing the system to mint the user more debt token than a user should hold, then they owe.
We are miniting a double interest
user balance 2000 USD
index 1
rate now 1.1
user calls to repay 1000 USD more debt tokens (borrow)
balance of returns 2200 USD debt owed.
but during the balance increase
2200(1.1) - 2000(1) = 420 USD instead of the 200 USD actually owed by the user.
Using Aave as a reference again , see implementation =>
* @notice Calculates the increase in balance since the last user interaction
* @param user The address of the user for which the interest is being accumulated
* @return The previous principal balance
* @return The new principal balance
* @return The balance increase
*/
function _calculateBalanceIncrease(
address user
) internal view returns (uint256, uint256, uint256) {
uint256 previousPrincipalBalance = super.balanceOf(user);
if (previousPrincipalBalance == 0) {
return (0, 0, 0);
}
uint256 newPrincipalBalance = balanceOf(user);
return (
previousPrincipalBalance,
newPrincipalBalance,
newPrincipalBalance - previousPrincipalBalance
);
}
Impact
Wrong calculation of the balance increase of the debt token for a user.
Tools Used
Manual review
Recommendations
Correct this by using the scaled balance of the user in the current calculations