Core Contracts

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

Incorrect Handling of Scaled Values in Liquidation Flow Leads to Debt Settlement Errors

Description

The liquidation flow across StabilityPool, LendingPool, and DebtToken contracts contains critical mismatches in how scaled and non-scaled values are handled, potentially leading to incorrect debt settlements and asset transfers.

Affected code

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/StabilityPool/StabilityPool.sol#L440-L470

/**
* @notice Liquidates a borrower's position.
* @dev This function can only be called by a manager or the owner when the contract is not paused.
* @param userAddress The address of the borrower to liquidate.
* @custom:throws InvalidAmount If the user's debt is zero.
* @custom:throws InsufficientBalance If the Stability Pool doesn't have enough crvUSD to cover the debt.
* @custom:throws ApprovalFailed If the approval of crvUSD transfer to LendingPool fails.
* @custom:emits BorrowerLiquidated when the liquidation is successful.
*/
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
// Approve the LendingPool to transfer the debt amount
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
lendingPool.updateState();
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/LendingPool/LendingPool.sol#L496-L540

/**
* @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();
// 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
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
// 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));
}

Vulnerability details

During the analysis of the liquidation flow, several significant issues were identified in how values are handled between contracts. The primary concern lies in the LendingPool contract where amountScaled returned from the burn() function is actually the non-scaled amount, creating a fundamental mismatch in value handling. Additionally, amountBurned is incorrectly used to reduce user.scaledDebtBalance, despite representing the scaled amount. The token transfer mechanism compounds these issues by using amountScaled, which is not the correct value for the transfer operation.

These inconsistencies create a cascade of potential problems throughout the system. The immediate impact includes incorrect debt settlements during liquidations and potential loss of funds due to incorrect transfer amounts. On a broader scale, these mismatches can lead to system-wide accounting errors that accumulate over time. The most concerning outcome is the possibility of protocol insolvency due to persistent debt and collateral mismatches that could arise from these calculation errors.

Tools Used

Manual Review

Recommended Mitigation Steps

  1. First, the finalizeLiquidation function should be modified to properly handle scaled and non-scaled values throughout its execution. The function should explicitly use the correct scaled amount for transfers and update the user's scaled debt balance appropriately.

function finalizeLiquidation(address userAddress) {
// ...
(uint256 amount, uint256 newTotalSupply, uint256 amountScaled, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Use correct scaled amount for transfer
IERC20(...).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
// Update with correct scaled value
user.scaledDebtBalance -= amountScaled;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::finalizeLiquidation passes normalized userDebt to DebtToken::burn which compares against scaled balance, causing incomplete debt clearance while taking all collateral

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::finalizeLiquidation passes normalized userDebt to DebtToken::burn which compares against scaled balance, causing incomplete debt clearance while taking all collateral

Support

FAQs

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