Core Contracts

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

Concurrent Oracle Fulfillments Overwrite House IDs, which leads to Incorrect Pricing

Summary

The RAACHousePriceOracle contract updates house prices using a single storage variable lastHouseId, which gets overwritten if multiple oracle requests are processed concurrently and fulfilled out of order. When the wrong houseId is used in a fulfillment callback, the price for one house can overwrite the price for another. This leads to incorrect collateral valuations in LendingPool, allowing for unauthorized or inaccurate borrowing/repayment actions.

Vulnerability Details

In RAACHousePriceOracle, the _beforeFulfill function stores the requested house ID in lastHouseId:

function _beforeFulfill(string[] calldata args) internal override {
lastHouseId = args[0].stringToUint();
}

When another request is sent before the first is fulfilled, lastHouseId gets overwritten by the second request’s ID.

And then the _processResponce function sets the incorrect pricing related to lastPriceId:

function _processResponse(bytes memory response) internal override {
uint256 price = abi.decode(response, (uint256));
housePrices.setHousePrice(lastHouseId, price);
emit HousePriceUpdated(lastHouseId, price);
}

Impact

  • In LendingPool, collateral valuation relies on priceOracle.getLatestPrice(tokenId). A mismatched or wrong price can either allow borrowers to over-borrow (leading to protocol shortfall) or cause unnecessary liquidations (hurting honest users).

  • Under/Over-Collateralization
    An NFT’s real price might be replaced by another NFT’s price, skewing the user’s health factor. This can systematically undermine the lending system.

  • Systemic Risk
    Because LendingPool uses the mispriced collateral across borrowing, liquidation, and withdrawal processes, widespread concurrency issues can degrade the entire protocol’s solvency and trust.

Tools Used

Manual Review.

PoC

Add the test bellow at the end of the file "test/unit/core/oracles/RAACHousePriceOracle.test.js".

it("Should demonstrate concurrency bug: second request overwrites the first house ID when fulfilled out of order", async function () {
// Create two distinct houses:
const houseId1 = 111;
const houseId2 = 222;
// Two distinct prices:
const price1 = 111111;
const price2 = 222222;
// -------------------------------------------------------------------------------------------
// STEP 1: Send Request #1 (houseId1)
// -------------------------------------------------------------------------------------------
const tx1 = await testOracle.sendRequest(
"return 111111;", // Some JS source that returns 111111
1, // secretsLocation
"0x", // no secrets
[houseId1.toString()], // string[] args (house ID)
[], // bytes[] bytesArgs
1, // subscriptionId
200000 // callbackGasLimit
);
await tx1.wait();
// Store the requestId from the oracle contract
const requestId1 = await testOracle.s_lastRequestId();
// -------------------------------------------------------------------------------------------
// STEP 2: Send Request #2 (houseId2)
// -------------------------------------------------------------------------------------------
const tx2 = await testOracle.sendRequest(
"return 222222;", // Some JS source that returns 222222
1,
"0x",
[houseId2.toString()],
[],
1,
200000
);
await tx2.wait();
const requestId2 = await testOracle.s_lastRequestId();
// Note: at this point, testOracle.lastHouseId() should be houseId2,
// because the second sendRequest overwrote the first one.
// -------------------------------------------------------------------------------------------
// STEP 3: Fulfill request #2 BEFORE request #1 (out of order!)
// -------------------------------------------------------------------------------------------
const response2 = new ethers.AbiCoder().encode(["uint256"], [price2]);
await mockOracle.fulfillRequest(
await testOracle.getAddress(),
requestId2,
response2,
"0x"
);
// House #2 should get price2 = 222222
// (We expect housePrices.getLatestPrice(222) => 222222)
// -------------------------------------------------------------------------------------------
// STEP 4: Fulfill request #1 second
// -------------------------------------------------------------------------------------------
const response1 = new ethers.AbiCoder().encode(["uint256"], [price1]);
await mockOracle.fulfillRequest(
await testOracle.getAddress(),
requestId1,
response1,
"0x"
);
// Because the oracle contract only has a single storage variable for houseId,
// fulfilling #1 AFTER #2 can accidentally overwrite the price for the second house
// or incorrectly assign price1 to the second house.
// -------------------------------------------------------------------------------------------
// STEP 5: Check final prices
// -------------------------------------------------------------------------------------------
const [priceForHouse1] = await housePrices.getLatestPrice(houseId1);
const [priceForHouse2] = await housePrices.getLatestPrice(houseId2);
console.log("Price for House #1 =>", priceForHouse1.toString());
console.log("Price for House #2 =>", priceForHouse2.toString());
// If the bug occurs, you'll see:
// - House #2 ended up with price1, or
// - House #1 never got updated
// - So the below expectations will fail
expect(priceForHouse1).to.equal(111111); // fails because it's 0
expect(priceForHouse2).to.equal(222222); // fails because it's 111111
});

Logs:

1) RAACHousePriceOracle (MockedTest)
Should demonstrate concurrency bug: second request overwrites the first house ID when fulfilled out of order:
AssertionError: expected 0 to equal 111111.
+ expected - actual
-0
+111111

The test shows that the House #1 price didn't get updated and price of House #2 is the price of House #1

Recommendations

One possible mitigation in RACCHousePriceOracle is to pass as input the requestId in _beforeFulfill and _processResponse, and have a mapping to store requestId -> houseId.

Updates

Lead Judging Commences

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

Oracle Race Condition in RAACHousePriceOracle causes price misassignment between NFTs

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

Oracle Race Condition in RAACHousePriceOracle causes price misassignment between NFTs

Support

FAQs

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