Core Contracts

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

lastUpdateTimestamp in RAACHausePrices has no connection to a specific asset but is updated globally

Description

RAACHausePrices::setHousePrice is supposed to update the price of an asset and update the timestamp when said asset has been updated last. The current implementation though updates uint256 public lastUpdateTimestamp; globally.

Vulnerable Code

RAACHausePrices:

uint256 public lastUpdateTimestamp;

RAACHausePrices::setHousePrice:

/**
* @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);
}

Looking at the code snippets we can already see quite clearly, that the intended functionality, to update the last price update timestamp individually is lacking, but the lastUpdateTimestamp for all NFTs will be updated any time any NFT will be updated.

PoC

Since the PoC is a foundry test I have added a Makefile at the end of this report to simplify installation for your convenience. Otherwise if console commands would be prefered:

First run: npm install --save-dev @nomicfoundation/hardhat-foundry

Second add: require("@nomicfoundation/hardhat-foundry"); on top of the Hardhat.Config file in the projects root directory.

Third run: npx hardhat init-foundry

And lastly, you will encounter one of the mock contracts throwing an error during compilation, this error can be circumvented by commenting out the code in entirety (ReserveLibraryMocks.sol).

And the test should be good to go:

After following above steps copy & paste the following code into ./test/invariant/PoC.t.sol and run forge test --mt test_PocTimestampOfPriceUpdatesGlobal -vv

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {StabilityPool} from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {LendingPool} from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import {CrvUSDToken} from "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import {RAACHousePrices} from "../../contracts/core/oracles/RAACHousePriceOracle.sol";
import {RAACNFT} from "../../contracts/core/tokens/RAACNFT.sol";
import {RToken} from "../../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../../contracts/core/tokens/DEToken.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {RAACMinter} from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract PoC is Test {
StabilityPool public stabilityPool;
LendingPool public lendingPool;
CrvUSDToken public crvusd;
RAACHousePrices public raacHousePrices;
RAACNFT public raacNFT;
RToken public rToken;
DebtToken public debtToken;
DEToken public deToken;
RAACToken public raacToken;
RAACMinter public raacMinter;
address owner;
address oracle;
address user1;
address user2;
address user3;
uint256 constant STARTING_TIME = 1641070800;
uint256 public currentBlockTimestamp;
uint256 constant WAD = 1e18;
uint256 constant RAY = 1e27;
function setUp() public {
vm.warp(STARTING_TIME);
currentBlockTimestamp = block.timestamp;
owner = address(this);
oracle = makeAddr("oracle");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
uint256 initialPrimeRate = 0.1e27;
raacHousePrices = new RAACHousePrices(owner);
vm.prank(owner);
raacHousePrices.setOracle(oracle);
crvusd = new CrvUSDToken(owner);
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
vm.prank(owner);
crvusd.setMinter(owner);
vm.prank(owner);
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
stabilityPool = new StabilityPool(address(owner));
deToken.setStabilityPool(address(stabilityPool));
raacToken = new RAACToken(owner, 0, 0);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner);
stabilityPool.initialize(address(rToken), address(deToken), address(raacToken), address(raacMinter), address(crvusd), address(lendingPool));
vm.prank(owner);
raacToken.setMinter(address(raacMinter));
crvusd.mint(address(attacker), type(uint128).max);
crvusd.mint(user1, type(uint128).max);
crvusd.mint(user2, type(uint128).max);
crvusd.mint(user3, type(uint128).max);
}
function test_PocTimestampOfPriceUpdatesGlobal() public {
// Set the timestamp of the first price update
vm.warp(14231234);
vm.startPrank(oracle);
raacHousePrices.setHousePrice(1, 10e18);
vm.stopPrank();
vm.startPrank(user1);
crvusd.approve(address(raacNFT), 10e18);
raacNFT.mint(1, 10e18);
vm.stopPrank();
(, uint256 lastUpdateId1) = raacHousePrices.getLatestPrice(1);
// Confirm the timestamp with log and assert
console.log(lastUpdateId1);
assertEq(lastUpdateId1, 14231234);
// Now we warp in the future and update the price of another NFT (ID2)
vm.warp(14231900);
vm.startPrank(oracle);
raacHousePrices.setHousePrice(2, 10e18);
vm.stopPrank();
// Fetch the last update timestamp of NFT ID1
(, lastUpdateId1) = raacHousePrices.getLatestPrice(1);
// And here we see that it was indeed updated, while we actually updated ID2
console.log(lastUpdateId1);
assertEq(14231900, lastUpdateId1);
}
}

Running that test produces the following log:

Ran 1 test for test/invariant/PoC.t.sol:PoC
[PASS] test_PocTimestampOfPriceUpdatesGlobal() (gas: 279897)
Logs:
14231234
14231900
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 17.09ms (1.68ms CPU time)

Showcasing indeed, that the price update of NFT ID 2 changed the last update timestamp of ID1.

Impact

In the current implementation it is basically impossible to check when exactly a specific tokenId has been updated, because as soon as any ID will be updated, all prices will seem to be updated as well, even it is not true.
The implications are relatively obvious, since there can not be any validation on chain if the price is stale for too long (and even off-chain would be difficult since the state simply will be read wrong by off-chain tooling), it is easily possible that many prices are stale, which results in users either missing out on potential higher credit lines or, which is worse, users would potentially be due for liquidation / can borrow more than they should harming the solvency of the protocol.
Looking at the natspec, it also seems this functionality was simply forgotten.

With a High Likelihood, since the issue occurs at any price update and a High impact, due to the fact that the protocol might be carrying unknowingly bad debt, I rate the total severity as High.

Used Tools

Manual Review

Recommended Mitigation

A possible mitigation would include simply connecting a specific asset ID to the timestamp like shown here:

contract RAACHousePrices is Ownable {
- mapping(uint256 => uint256) public tokenToHousePrice;
- uint256 public lastUpdateTimestamp;
+ struct PriceData {
+ uint256 price;
+ uint256 lastUpdateTimestamp;
+ }
+ mapping(uint256 => PriceData) public tokenToPriceData;
address public oracle;
/// *snip* ///
function setHousePrice(
uint256 _tokenId,
uint256 _amount
) external onlyOracle {
- tokenToHousePrice[_tokenId] = _amount;
- lastUpdateTimestamp = block.timestamp;
+ tokenToPriceData[_tokenId] = PriceData({
+ price: _amount,
+ lastUpdateTimestamp: block.timestamp
+ });
emit PriceUpdated(_tokenId, _amount);
}
/// *snip* ///
}

Appendix

Copy the following import into your Hardhat.Config file in the projects root dir:
require("@nomicfoundation/hardhat-foundry");

Paste the following into a new file "Makefile" into the projects root directory:

.PHONY: install-foundry init-foundry all
install-foundry:
npm install --save-dev @nomicfoundation/hardhat-foundry
init-foundry: install-foundry
npx hardhat init-foundry
# Default target that runs everything in sequence
all: install-foundry init-foundry

And run make all

Updates

Lead Judging Commences

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