Core Contracts

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

RAACHousePrice’s Single Global Timestamp Misleads LendingPool About Freshness for Each NFT

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

RAACHousePrice uses a single lastUpdateTimestamp for all token IDs, misleading the LendingPool about how up-to-date each NFT’s price is. Attackers can let other tokens keep “updating” the global timestamp while never refreshing their own NFT’s stale or inflated price. Switching to per-token timestamps ensures each NFT’s update time is tracked individually, preventing stale or never-updated collateral data from being treated as fresh.

Overview

  1. RAACHousePrice stores a single:

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

    The contract lumps all token IDs under one lastUpdateTimestamp, even if _tokenId is just one specific NFT being updated.

  2. LendingPool calls priceOracle.getLatestPrice(tokenId), receiving (price, lastTimestamp). Because lastUpdateTimestamp is used for all tokens, the “timestamp” is the time of the most recent update for any NFT, not necessarily the NFT in question. LendingPool or other consumers might treat that “lastTimestamp” as fresh for every token ID.

  3. Result: A token ID that never had its price set or had it last set weeks ago can appear “fresh” because lastUpdateTimestamp was updated by some other token’s setHousePrice(...) call. This can:

    • Let an attacker borrow more than they should if their NFT’s actual price is stale or artificially inflated from a long time ago.

    • Allow silent subversion of the LendingPool’s stale data checks, because the global timestamp is advanced whenever any token is updated, not necessarily the one being used for collateral.

Attack / Proof of Concept

  1. Token #123 has an old price from 2 months ago, last set at T0.

  2. Token #456 is updated now at T1 (recent block), so RAACHousePrice.setHousePrice(456, newAmount) sets lastUpdateTimestamp = T1.

  3. User uses Token #123 as collateral in the LendingPool:

    (uint256 price, uint256 lastUpdated) = housePriceOracle.getLatestPrice(123);
    • The code sees lastUpdated = T1 (the global lastUpdateTimestamp), incorrectly believing the price for NFT #123 is fresh.

    • The user can overborrow or rely on an out-of-date price if the actual #123 price is inflated or never updated.

Impact

Stale or Never Updated Price: The LendingPool (and other consumers) incorrectly deems the data “recent,” ignoring that the specific token’s tokenToHousePrice[_tokenId] might be from months ago. This can lead to severe mispricing of collateral and over-lending.

Exploitable Over Borrowing: Attackers deliberately keep a high old price for their NFT #123, never calling setHousePrice for that ID. Meanwhile, someone else updates a different NFT. The global lastUpdateTimestamp is advanced, letting #123 appear “fresh” even though it’s actually stale and possibly inflated.

Undermines Collateral Security: The entire system presupposes that lastUpdateTimestamp reveals how recently each NFT’s price was updated. Instead, it lumps all token IDs under a single timestamp.

Recommendation

  1. Per-Token Timestamps

    • In RAACHousePrice, store lastUpdatedTimestamp[_tokenId] = block.timestamp for each token. So each NFT’s price data has a unique update time.

    • Then getLatestPrice(_tokenId) can return (tokenToHousePrice[_tokenId], lastUpdatedTimestamp[_tokenId]).

  2. Remove or Repurpose lastUpdateTimestamp

    • If the contract wants a global “any price updated” timestamp for some reason, rename it to something like globalLastUpdate. But do not feed it to the LendingPool as if it’s the NFT’s personal timestamp.

    • Instead, supply the per-token timestamp to confirm the actual data’s freshness.

  3. Enhance LendingPool

    • The LendingPool can check whether block.timestamp - tokenSpecificLastUpdated[_tokenId] < someStaleThreshold to ensure each NFT’s data is truly fresh. If it’s older than a threshold, revert or require an updated price.

Updates

Lead Judging Commences

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

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

Give us feedback!