Core Contracts

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

Using Fake Stability Pools to Steal Liquidated Collateral in LendingPool.sol

Summary

The LendingPool contract in the RAAC protocol lacks proper validation of the Stability Pool address, allowing an attacker to register and use a forged/fake Stability Pool to redirect liquidations to their fraudulent contract. This enables attackers to steal liquidated collateral instead of repaying the outstanding debt, leading to protocol insolvency.

Vulnerability Details

The finalizeLiquidation() function currently lacks verification that ensures:

  1. The Stability Pool processing the liquidation is authorized.

  2. The debt repayment actually benefits the protocol.

As a result, an attacker can deploy a fake Stability Pool contract, redirect liquidations to their fraudulent contract, and steal the collateral.

How the Attack Works

  1. An NFT-backed loan enters liquidation.

  2. The attacker registers a fake Stability Pool (if the contract allows modifications).

  3. They trigger the liquidation process, causing the fake pool to "receive" the collateral.

  4. The attacker withdraws the stolen NFT collateral, leaving a bad debt in the protocol.

Problematic Code in finalizeLiquidation()

/**
* @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));
}

The function assumes that msg.sender == stabilityPool, but does not validate if the Stability Pool is legitimate.

  • If the stabilityPool address is updated to an attacker-controlled contract, liquidated collateral will be sent to the wrong address.

Example Attack Scenario

  1. A user defaults on a loan, and their NFT is flagged for liquidation.

  2. The attacker deploys a fake Stability Pool contract and sets it as the active Stability Pool.

  3. The attacker calls finalizeLiquidation(), sending liquidated NFTs to their fake pool.

  4. The attacker withdraws the stolen NFTs from their contract and sells them for profit.

  5. The protocol does not recover debt, leading to bad debt accumulation.

Impact

Unauthorized Collateral Theft: Attackers can steal user NFTs and liquidated assets.

Protocol Insolvency: The protocol fails to repay debt, resulting in bad debt accumulation.

Loss of User Funds: Honest users are unable to reclaim their collateral after repaying debts.

Tools Used

Manual Review

Recommendations

1. Verify the Stability Pool Address

Implement whitelist validation to ensure that only approved Stability Pools can process liquidations:

modifier onlyRegisteredStabilityPool() {
require(registeredStabilityPools[msg.sender], "Invalid Stability Pool");
_;
}
  • Use this modifier in finalizeLiquidation():

function finalizeLiquidation(address userAddress)
external
nonReentrant
onlyRegisteredStabilityPool
{

2. Ensure Collateral and Debt Are Transferred to the Correct Pool

Add a check to verify that liquidated collateral and debt match the expected tokens:

require(
reserve.reserveAssetAddress == IERC20(reserve.reserveRTokenAddress).balanceOf(address(this)),
"Invalid token transfer"
);

This prevents fake pools from receiving unauthorized assets.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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