Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Invalid

Users will not be able to repay their loan because we do not consider balance increase in the burning process, this also makes liquidation impossible.

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];
// Update reserve state before repayment
ReserveLibrary.updateReserveState(reserve, rateData);
// Calculate the user's debt (for the onBehalfOf address)
uint256 userDebt = IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf);
uint256 userScaledDebt = userDebt.rayDiv(reserve.usageIndex);
// If amount is greater than userDebt, cap it at userDebt
uint256 actualRepayAmount = amount > userScaledDebt ? userScaledDebt : amount; //bug capped at scaled balance instad of debt
uint256 scaledAmount = actualRepayAmount.rayDiv(reserve.usageIndex); // bug double division wahala wa oo
// Burn DebtTokens from the user whose debt is being repaid (onBehalfOf)
// is not actualRepayAmount because we want to allow paying extra dust and we will then cap there
@audit>>. (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
// Transfer reserve assets from the caller (msg.sender) to the reserve
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
reserve.totalUsage = newTotalSupply;
user.scaledDebtBalance -= amountBurned;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit Repay(msg.sender, onBehalfOf, actualRepayAmount); // bug with actual
}

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); // there is a bug here though returns wrong balance // bug we did not use this one
@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()); //bug Will revert we never mint new balance to user medium
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 =>

/// @inheritdoc IStableDebtToken
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;
// Since the total supply and each single user debt accrue separately,
// there might be accumulation errors so that the last borrower repaying
// might actually try to repay more than the available debt supply.
// In this case we simply set the total supply and the avg stable rate to 0
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());
// For the same reason described above, when the last user is repaying it might
// happen that user rate * user balance > avg rate * total supply. In that case,
// we simply set the avg rate to 0
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 {
//solium-disable-next-line
_timestamps[from] = uint40(block.timestamp);
}
//solium-disable-next-line
_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

// Burn DebtTokens from the user
@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);// balance increase not considered debt remains
amount = amount; // balance increase not considered debt remains
}
_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()); // balance increase not considered debt remains
emit Burn(from, amountScaled, index); // balance increase not considered debt remains
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.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

DebtToken::burn calculates balanceIncrease (interest) but never applies it, allowing borrowers to repay loans without paying accrued interest

Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

DebtToken::burn calculates balanceIncrease (interest) but never applies it, allowing borrowers to repay loans without paying accrued interest

Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.

Support

FAQs

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