Core Contracts

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

Broken NFT price staleness check

Summary

  1. LendingPool does not check the timestamp returned by RAACHousePrices#getLatestPrice.

  2. RAACHousePrices has only global lastUpdateTimestamp, and does not provide a way to determine if individual _tokenIds' price is fresh or not.

Vulnerability Details

To determine the price of NFT collateral, LendingPool uses getLatestPrice

function getNFTPrice(uint256 tokenId) public view returns (uint256) {
(uint256 price, uint256 lastUpdateTimestamp) = priceOracle.getLatestPrice(tokenId);
if (price == 0) revert InvalidNFTPrice();
return price;
}

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/primitives/RAACHousePrices.sol#L27-L38

The purpose of lastUpdateTimestamp is to make sure the price is not stale (which is utilized by LendingPool for calculating the value of NFT collateral).

However, each time price of any tokenId is updated, lastUpdateTimestamp is updated. Therefore, prices of all tokens except the last updated one could be stale, but their price will still be considered fresh by the LendingPool.

Moreover, lastUpdateTimestamp is never validated in getNFTprice, therefore stale prices can be accepted even if the issue with lastUpdateTimestamp is fixed.

Impact

Old price can be consumed for NFT collateral calculation.

Recommendations

  1. LendingPool#getNFTPrice should validate that returned timestamp from getLatestPrice is not too old.

  2. In RAACHousePrices, lastUpdateTimestamp should be a tokenId => timestamp mapping.

- uint256 public lastUpdateTimestamp;
+ mapping(uint256 => uint256) public lastUpdateTimestamp;
function getLatestPrice(
uint256 _tokenId
) external view returns (uint256, uint256) {
- return (tokenToHousePrice[_tokenId], lastUpdateTimestamp);
+ return (tokenToHousePrice[_tokenId], lastUpdateTimestamp[_tokenId]);
}
function setHousePrice(
uint256 _tokenId,
uint256 _amount
) external onlyOracle {
tokenToHousePrice[_tokenId] = _amount;
- lastUpdateTimestamp = block.timestamp;
+ lastUpdateTimestamp[_tokenId] = block.timestamp;
emit PriceUpdated(_tokenId, _amount);
}
Updates

Lead Judging Commences

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

RAACHousePrices uses a single global lastUpdateTimestamp for all NFTs instead of per-token tracking, causing misleading price freshness data

LendingPool::getNFTPrice or getPrimeRate doesn't validate timestamp staleness despite claiming to, allowing users to exploit outdated collateral values during price drops

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

RAACHousePrices uses a single global lastUpdateTimestamp for all NFTs instead of per-token tracking, causing misleading price freshness data

LendingPool::getNFTPrice or getPrimeRate doesn't validate timestamp staleness despite claiming to, allowing users to exploit outdated collateral values during price drops

Support

FAQs

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