Core Contracts

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

Inaccurate Global Timestamp in RAACHousePrices Leads to Stale Price Exploits

Summary

The RAACHousePrices contract maintains a single global timestamp (lastUpdateTimestamp) for price updates instead of storing per-NFT update timestamps. When an oracle updates the price of one NFT, all NFTs falsely appear as recently updated, potentially allowing attackers to use outdated or manipulated prices for loans, liquidations, or collateral valuations.

This issue undermines the protocol's ability to verify price freshness and could result in severe financial loss if attackers exploit outdated high valuations to overborrow or avoid liquidations.

(Note: This bug is hereby submitted assuming the staleness check will be implemented when querying getLatestPrice() as I have separately submitted a report for it.)

Vulnerability Details

The function setHousePrice() updates lastUpdateTimestamp globally instead of tracking timestamps per NFT:

RAACHousePrices.sol#L49-L56

function setHousePrice(
uint256 _tokenId,
uint256 _amount
) external onlyOracle {
tokenToHousePrice[_tokenId] = _amount;
lastUpdateTimestamp = block.timestamp;
emit PriceUpdated(_tokenId, _amount);
}

The function getLatestPrice() does not verify if the specific NFT's price is stale, making it appear that all NFTs were updated at the same time:

RAACHousePrices.sol#L34-L38

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

How an Attacker Could Exploit This

Step1. Obtain an Overvalued NFT:

  • Suppose an NFT was priced at $10,000 three months ago but has since lost value to $2,000.

  • The protocol expects a fresh update before allowing loans based on this NFT’s value.

Step2. Trigger an Irrelevant NFT Price Update:

  • Price update of an unrelated NFT in the system transpires moments beforehand.

  • The global timestamp is updated, falsely making it appear that all NFTs, including the overpriced one, were recently updated.

Step3. Use the Outdated NFT Price for Loans or Avoid Liquidation:

  • The attacker uses the outdated $10,000 price to borrow more funds than they should be allowed.

  • Alternatively, they could avoid liquidation, since the protocol believes the outdated NFT still holds value.

Regardless, it doesn't matter when the position is liquidated later as the attacker has already maximized the arbitrage as restrained by the liquidationThreshold check beforehand.

LendingPool.sol#L343-L346

// Ensure the user has enough collateral to cover the new debt
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}

Note: The reverse is also true where NFT prices have appreciated and a position should have avoided liquidation via a revert from the staleness check, e.g. block.timestamp - lastUpdateTimestamp > MAX_PRICE_AGE, had per-token timestamp been properly assigned.

Impact

This issue can be actively exploited to mislead the system into believing prices are fresh, leading to incorrect lending (deceptive over-collateralization) or faulty liquidations in the opposite scenario. Since NFT prices can fluctuate significantly, failing to track per-token timestamps can result in millions in bad debt or unjust liquidations from the opposite end.

Tools Used

Manual

Recommendations

Consider making the following refactoring:

RAACHousePrices.sol#L34-L56

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

And, don't forget to modify getNFTPrice() in LendingPool.sol to reject stale prices via something like:

uint256 constant MAX_PRICE_AGE = 120 days;
function getNFTPrice(uint256 tokenId) public view returns (uint256) {
(uint256 price, uint256 lastUpdateTimestamp) = priceOracle.getLatestPrice(tokenId);
if (price == 0 || block.timestamp - lastUpdateTimestamp > MAX_PRICE_AGE) {
revert InvalidNFTPrice();
}
return price;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 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

Support

FAQs

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