Core Contracts

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

Stale NFT prices result in Incorrect calculations of collateral value

Summary

The RAACHousePrices contract could return outdated NFT prices
resulting in Incorrect collateral value calculations due to

i) Usage of global lastUpdateTimestamp for all NFTs.
ii) Missing freshness check resulting in stale prices.
iii) Missing implementation of UPDATE_INTERVAL

Vulnerability Details

The contract assigns the address of oracle to RAACHousePrices

RAACHousePrices is resposnsible for interacting with the oracle to maintain the latest NFT prices.
Everytime a price update is requested
RAACHousePriceOracle calls back setHousePrice
which updates the tokenToHousePrice mapping and a global lastUpdateTimestamp.

The core issues are,
i) the contract maintains a global lastUpdateTimestamp
instead of a tokenID specific update timestamp ,and

ii) there is no stale price check.
even though the IRAACHousePrices interface suggests a MAX_PRICE_AGE.
It is not implemeneted anywhere.

iii) Missing implementation of UPDATE_INTERVAL:
The time interval between consecutive price updates.

In this scenario.

A price update for one NFT updates the global timestamp lastUpdateTimestamp
making it look like all NFTs in the mapping carry the latest price,
even if the prices for some TokenIds could be outdated.

// RAACHousePrices Contract
mapping(uint256 => uint256) public tokenToHousePrice;
uint256 public lastUpdateTimestamp; //<<-- global timestamp
// setLatestPrice called by oracle
function setHousePrice(uint256 _tokenId,uint256 _amount) external onlyOracle {
tokenToHousePrice[_tokenId] = _amount;
lastUpdateTimestamp = block.timestamp; //<<-- updates the global timestamp
emit PriceUpdated(_tokenId, _amount);
}
// getLatestPrice
function getLatestPrice(uint256 _tokenId) external view returns (uint256, uint256) {
// returns a global timestamp
return (tokenToHousePrice[_tokenId], lastUpdateTimestamp);
}

When an user tries to withdraw their NFTs withdrawNFT,
the getNFTPrice function is used to fetch the latest prices of every NFT tokenId held by the user
which in turn is used to calculate the overall collateral value.

//withdrawNFT
uint256 collateralValue = getUserCollateralValue(msg.sender);
//getUserCollateralValue
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
uint256 price = getNFTPrice(tokenId);
totalValue += price;
}
//getNFTPrice
function getNFTPrice(uint256 tokenId) public view returns (uint256) {
(uint256 price, uint256 lastUpdateTimestamp) = priceOracle.getLatestPrice(tokenId);
if (price == 0) revert InvalidNFTPrice();
return price;
}

But since the getLatestPrice() function in RAACHousePrices returns a global lastUpdateTimestamp
instead of a token-specific last update timestamp.

Furthermore,
there are no checks in getNFTPrice() to ensure the returned lastUpdateTimestamp falls within the acceptable limits of the current block.timestamp suggested by MAX_PRICE_AGE so that the protocol isn't using stale data.


Example exploit scenarios

Manipulating Liquidations:

  • A user could deposit an NFT that was priced high but has since lost value.

  • If the oracle hasn’t updated that specific NFT’s price, getUserCollateralValue() will overestimate the user's collateral.

  • This prevents liquidation even when the user's debt is higher than their actual collateral.

Over-Borrowing Risk:

  • A user might be able to borrow more than their collateral's true worth if the NFT price remains outdated.

  • If the NFT’s price later updates to a lower value, the system will suddenly become undercollateralized, leading to bad debt.mpact

Impact

Stale NFT prices result in wrong collateral value calculation
which in turn results in several issues such as
a) bad debt
b) over or under‑collateralizations and
c) mis-triggered liquidations and more.

Impact : High

Likelihood : High

Recommendations

NFTs should have token specific lastUpdateTimestampand
Checks should be made to restrict stale prices by implementing MAX_PRICE_AGE.

Implement UPDATE_INTERVAL**, **The time interval between consecutive price updates.

An example implementation below

// RAACHousePrices
mapping(uint256 => uint256) public tokenToHousePrice;
mapping(uint256 => uint256) public tokenLastUpdated;
//getLatestPrice
function getLatestPrice(
uint256 _tokenId
) external view returns (uint256, uint256) {
return (tokenToHousePrice[_tokenId], tokenLastUpdated[_tokenId]); // Return correct timestamp
}
//setHousePrice
function setHousePrice(
uint256 _tokenId,
uint256 _amount
) external onlyOracle {
tokenToHousePrice[_tokenId] = _amount;
tokenLastUpdated[_tokenId] = block.timestamp; // Update timestamp for this token only
emit PriceUpdated(_tokenId, _amount);
}
function getNFTPrice(uint256 tokenId) public view returns (uint256) {
(uint256 price, uint256 lastUpdateTimestamp) = priceOracle.getLatestPrice(tokenId);
if (price == 0) revert InvalidNFTPrice();
// Check price freshness
if (block.timestamp > lastUpdateTimestamp + priceOracle.MAX_PRICE_AGE) {
revert PriceOutdated();
}
return price;
}


Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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 6 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.