Race condition vulnerability in RAACHousePriceOracle where multiple oracle requests can lead to incorrect house price updates due to shared state variable (lastHouseId).
The RAACHousePriceOracle contract contains a race condition vulnerability related to the lastHouseId state variable. This occurs because multiple concurrent Chainlink Functions requests can overwrite lastHouseId before previous responses are processed, leading to incorrect price updates.
Flow of Requests and Responses:
When sendRequest() is called, it triggers _beforeFulfill(args), which sets lastHouseId using the first argument (args[0]).
The request is sent to Chainlink, and the response is handled asynchronously by fulfillRequest().
The _processResponse() function uses lastHouseId to update the house price, assuming it matches the original request.
Race Condition Scenario:
Request A is sent with args[0] = 123. lastHouseId is set to 123.
Before Request A's response arrives, Request B is sent with args[0] = 456. lastHouseId is updated to 456.
If Response B is processed first, it correctly updates the price for house 456.
When Response A arrives later, lastHouseId is now 456, so the price for house 123 is erroneously applied to house 456.
Root Cause:
lastHouseId is a single global variable shared across all requests.
There is no mechanism to correlate responses with their original requests (e.g., request IDs or mapping).
If multiple requests are in flight, the final value of lastHouseId depends on the order of responses, not the order of requests.
Here is simplified example:
Alice calls sendRequest(123)
lastId = 123
Before response comes, Bob calls sendRequest(456)
lastId = 456
Bob's response arrives first:
fulfillRequest(100) → Sets prices[456] = 100 ✅
Alice's response arrives later:
fulfillRequest(200) → Sets prices[456] = 200 ❌
Alice's price was applied to Bob's ID!
The vulnerability has severe implications for the lending:
getUserCollateralValue() may return wrong total value. This could allow users to borrow more than their collateral allows or prevent legitimate borrowing due to undervalued collateral
borrow() function relies on collateral value checks. Wrong prices could bypass the check: collateralValue < userTotalDebt.percentMul(liquidationThreshold). Potential protocol insolvency risk
Blocked Withdrawals. withdrawNFT() might incorrectly block/allow NFT withdrawals
Users might be unable to withdraw their NFTs or withdraw when they shouldn't
Manual review
Store requestid and lastHouseId in a map.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.