Core Contracts

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

getNormalizedDebt will return a wrong Amount when Timedelta is 0.

Summary

Instead of returning the index of the position, the debt value is return leading to an error whenever we call to calculate a users debt

Vulnerability Details

The totalusage and total liquidity are the total asset and total debt respectively.

/**
* @notice Struct to hold reserve data.
* @dev All values are stored in RAY (27 decimal) precision.
*/
struct ReserveData {
address reserveRTokenAddress;
address reserveAssetAddress;
address reserveDebtTokenAddress;
uint256 totalLiquidity;
@audit>> uint256 totalUsage;
uint128 liquidityIndex;
@audit>> uint128 usageIndex;
uint40 lastUpdateTimestamp;
}


Call to get normalised income returns the correct index but debt is flawed

/**
* @notice Gets the normalized income of the reserve.
* @param reserve The reserve data.
* @return The normalized income (in RAY).
*/
function getNormalizedIncome(ReserveData storage reserve, ReserveRateData storage rateData) internal view returns (uint256) {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
@audit>> index >> return reserve.liquidityIndex;
}
@audit>> index >> return calculateLinearInterest(rateData.currentLiquidityRate, timeDelta, reserve.liquidityIndex).rayMul(reserve.liquidityIndex);
}
/**
* @notice Gets the normalized debt of the reserve.
* @param reserve The reserve data.
* @return The normalized debt (in underlying asset units).
*/
function getNormalizedDebt(ReserveData storage reserve, ReserveRateData storage rateData) internal view returns (uint256) {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
@audit>> returning debt instead of index>>> return reserve.totalUsage; // we dey return debt instead of usageindex jesus // bug high
}
@audit>> returns index >> return calculateCompoundedInterest(rateData.currentUsageRate, timeDelta).rayMul(reserve.usageIndex);
}

In the stability pool when we call to liquidate a position from the lending pool

Stability pool if call after an update

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);
@audit>> uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance(); // liquidation can fail why not try partial liquidation or implement flashloan liquidation cause a whale can never be liquidated
// Approve the LendingPool to transfer the debt amount
@audit>> wrong calculation used for approval>> bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

the above code multiplies the debt by the total debt divided by ray instead of getting the scaled user debt

Lending pool

/**
* @notice Allows the Stability Pool to finalize the liquidation after the grace period has expired
* @param userAddress The address of the user being liquidated
*/
function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation(); // see here
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
// Transfer NFTs to Stability Pool
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
delete user.nftTokenIds;
// Burn DebtTokens from the user
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Transfer reserve assets from Stability Pool to cover the debt
@audit>> transfer from stability pool >>> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled); // asset is sent in from the boss account satbility token
// Update user's scaled debt balance
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}

Looking at Aave's implementation also -

/**
* @notice Returns the ongoing normalized income for the reserve.
* @dev A value of 1e27 means there is no income. As time passes, the income is accrued
* @dev A value of 2*1e27 means for each unit of asset one unit of income has been accrued
* @param reserve The reserve object
* @return The normalized income, expressed in ray
*/
function getNormalizedIncome(
DataTypes.ReserveData storage reserve
) internal view returns (uint256) {
uint40 timestamp = reserve.lastUpdateTimestamp;
//solium-disable-next-line
if (timestamp == block.timestamp) {
//if the index was updated in the same block, no need to perform any calculation
return reserve.liquidityIndex;
} else {
return
MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul(
reserve.liquidityIndex
);
}
}
/**
* @notice Returns the ongoing normalized variable debt for the reserve.
* @dev A value of 1e27 means there is no debt. As time passes, the debt is accrued
* @dev A value of 2*1e27 means that for each unit of debt, one unit worth of interest has been accumulated
* @param reserve The reserve object
* @return The normalized variable debt, expressed in ray
*/
function getNormalizedDebt(
DataTypes.ReserveData storage reserve
) internal view returns (uint256) {
uint40 timestamp = reserve.lastUpdateTimestamp;
//solium-disable-next-line
if (timestamp == block.timestamp) {
//if the index was updated in the same block, no need to perform any calculation
@audit>>> return reserve.variableBorrowIndex;
} else {
return
MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul(
reserve.variableBorrowIndex
);
}
}

Impact

Wrong borrow rate, utilization rate and liquidity rate calculations.

Tools Used

Manual Review

Recommendations

Return the index of the debt and not the debt amount

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

getNormalizedDebt returns totalUsage (amount) instead of usageIndex (rate) when timeDelta < 1, breaking interest calculations across the protocol

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

getNormalizedDebt returns totalUsage (amount) instead of usageIndex (rate) when timeDelta < 1, breaking interest calculations across the protocol

Support

FAQs

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