Core Contracts

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

Lack of Staleness Check in NFT Price Retrieval Could Lead to Overvaluation or Liquidation Risks

Summary

The getNFTPrice function in Lending.sol is responsible for fetching the latest price of an NFT from the RAACHousePrices contract. While the function's NatSpec comment suggests that it checks for stale prices, the actual implementation only verifies whether the price is 0, without checking how recent the last price update was. This omission can lead to outdated NFT valuations being used, potentially mispricing loans or triggering unnecessary liquidations.

Vulnerability Details

First off, the sponsor contest discord has clarified that RAACHousePrices is updated via Chainlink Functions, pulling off-chain property valuation data from an audited API into the blockchain through a decentralized oracle network. The onlyOracle modifier ensures that only Chainlink oracle nodes can update house prices.

RAACHousePrices.sol#L42-L56

/**
* @notice Allows the owner to set the house price for a token
* @param _tokenId The ID of the RAAC token
* @param _amount The price to set for the house in USD
*
* Updates timestamp for each token individually
*/
function setHousePrice(
uint256 _tokenId,
uint256 _amount
) external onlyOracle {
tokenToHousePrice[_tokenId] = _amount;
lastUpdateTimestamp = block.timestamp;
emit PriceUpdated(_tokenId, _amount);
}

This allows external contracts querying the price of the NFT via:

RAACHousePrices.sol#L34-L38

function getLatestPrice(
uint256 _tokenId
) external view returns (uint256, uint256) {
return (tokenToHousePrice[_tokenId], lastUpdateTimestamp);
}

So when the function LendingPool.getNFTPrice() fetches the price of an NFT along with the timestamp of the last update, it only checks if the price is zero and does not validate whether the price is outdated:

LendingPool.sol#L591-L595

/**
* @notice Gets the current price of an NFT from the oracle
* @param tokenId The token ID of the NFT
* @return The price of the NFT
*
* Checks if the price is stale
*/
function getNFTPrice(uint256 tokenId) public view returns (uint256) {
(uint256 price, uint256 lastUpdateTimestamp) = priceOracle.getLatestPrice(tokenId);
if (price == 0) revert InvalidNFTPrice();
return price;
}

Now, unlike Chainlink Price Feeds that have built-in staleness safeguards with heartbeat updates and deviation thresholds, Chainlink functions is simply a generic off-chain computation service that fetches API data and off-ramps it onto the blockchain. The latter is devoid of the staleness safeguards.

The protocol team confirmed that house prices may only be updated once every several months (1-3 times a year). If the oracle does not update for an extended period, old prices might be used for loan calculations, affecting the security of the lending pool.

Impact

  • Overvaluation Risk: If an NFT’s value drops significantly but the stored price remains high, users may borrow more than they should, leading to under-collateralized loans.

  • Unfair Liquidations: If an NFT’s price has appreciated, but the system still relies on an outdated price, borrowers might get liquidated at an unfair rate.

  • Loss of Trust in the Protocol: If users lose assets due to outdated price data, the protocol’s credibility and user trust may decline.

Tools Used

Manual

Recommendations

To mitigate this issue, implement a configurable price staleness check in LendingPool.sol:

LendingPool.sol#L584-L595

+ uint256 public priceStalenessThreshold = 90 days; // Default value, adjustable
+ function setPriceStalenessThreshold(uint256 _threshold) external onlyOwner {
+ require(_threshold > 0, "Threshold must be greater than zero");
+ priceStalenessThreshold = _threshold;
+ }
function getNFTPrice(uint256 tokenId) public view returns (uint256) {
(uint256 price, uint256 lastUpdateTimestamp) = priceOracle.getLatestPrice(tokenId);
- if (price == 0) revert InvalidNFTPrice();
+ if (price == 0 || (block.timestamp - lastUpdateTimestamp > priceStalenessThreshold)) revert InvalidNFTPrice();
return price;
}
Updates

Lead Judging Commences

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

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 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

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.