Summary
The balance increase is not added to the user's debt or subtracted from the repayment amount hence a user will never be able to repay this amount as a call to actually repay it will revert when we call burn.
Vulnerability Details
Whenever a user tries to repay his loan or repays his loan if this loan is greater than 1 s the user will be obligated to repay the loan with interest but this fee is not added to the minted funds to the user hence when we burn we are burning an amount less than the amount we should
* @notice Internal function to repay borrowed reserve assets
* @param amount The amount to repay
* @param onBehalfOf The address of the user whose debt is being repaid. If address(0), msg.sender's debt is repaid.
* @dev This function allows users to repay their own debt or the debt of another user.
* The caller (msg.sender) provides the funds for repayment in both cases.
* If onBehalfOf is set to address(0), the function defaults to repaying the caller's own debt.
*/
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);
}
When we call burn we try to get the balance increase of the debt
e.g Alice borrowed 1000 USD at index 1
NOW she is repaying at index 1.1
She should repay 1100 USD (in interest + the loan amount.)
But this will not be the case as the increased balance is not subtracted from the amount to repay nor was it added to the user's debt balance hence any call to Burn the amount will cause Repay to always revert and this means such user can never evade liquidation.
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 >> returns the actual debt of the user 1100 USDC >>. uint256 userBalance = balanceOf(from);
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
@audit >> from minting time 100 USDC interestis owed >>> balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
@audit >> not decreased by the amount of interest>> amount = amount;
}
_userState[from].index = index.toUint128();
if(amount > userBalance){
@audit >> burn max balance 1100 usdc>> amount = userBalance;
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
@AUDIT>> BURN 1100USDC account But we minted 1000 USDC during mint>>> _burn(from, amount.toUint128());
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}
Though the user can repay part of their loan without repaying below DUST amount the user still will face initiation liquidation and after that a user can still never be able to repay successfully before the liquidation is finalized.
SEE Aave's implementation for the same code =>
function burn(
address from,
uint256 amount
) external virtual override onlyPool returns (uint256, uint256) {
(, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(from);
uint256 previousSupply = totalSupply();
uint256 nextAvgStableRate = 0;
uint256 nextSupply = 0;
uint256 userStableRate = _userState[from].additionalData;
if (previousSupply <= amount) {
_avgStableRate = 0;
_totalSupply = 0;
} else {
nextSupply = _totalSupply = previousSupply - amount;
uint256 firstTerm = uint256(_avgStableRate).rayMul(previousSupply.wadToRay());
uint256 secondTerm = userStableRate.rayMul(amount.wadToRay());
if (secondTerm >= firstTerm) {
nextAvgStableRate = _totalSupply = _avgStableRate = 0;
} else {
nextAvgStableRate = _avgStableRate = (
(firstTerm - secondTerm).rayDiv(nextSupply.wadToRay())
).toUint128();
}
}
if (amount == currentBalance) {
_userState[from].additionalData = 0;
_timestamps[from] = 0;
} else {
_timestamps[from] = uint40(block.timestamp);
}
_totalSupplyTimestamp = uint40(block.timestamp);
@audit>>> if (balanceIncrease > amount) {
@audit>>> uint256 amountToMint = balanceIncrease - amount;
@audit>>> _mint(from, amountToMint, previousSupply);
@audit>>> emit Transfer(address(0), from, amountToMint);
@audit>>> emit Mint(
from,
from,
amountToMint,
currentBalance,
balanceIncrease,
userStableRate,
nextAvgStableRate,
nextSupply
);
} else {
@audit>>> uint256 amountToBurn = amount - balanceIncrease;
@audit>>> _burn(from, amountToBurn, previousSupply);
emit Transfer(from, address(0), amountToBurn);
emit Burn(from, amountToBurn, currentBalance, balanceIncrease, nextAvgStableRate, nextSupply);
}
return (nextSupply, nextAvgStableRate);
}
Do not discard the interest (balance increase) during repayment and implement it in the calculation else the users will never be able to repay their loan completely.
During liquidation, we call
@audit (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
Not adding the balance we cause the burn amount call to revert,
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);
}
uint256 userBalance = balanceOf(from);
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
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();
@audit>> _burn(from, amount.toUint128());
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}
Impact
Inability to repay the loan with interest and the users will be subject to liquidation eventually. But all liquidation call also will fail. This also affects the total debt as it returns the total debt tokens minted in them of total supply not adding the balance increase means the utilization ratio will be affected, this affects the liquidity rate and borrow rate leading to a wrong index use and a total data corruption of the system.
Tools Used
Manual review
Recommendations
As done by Aave, catch the balance increase and implement a way to mint the user these tokens before we burn the debt tokens to repay.