The scrvUSD verifier relies on a blockhash oracle to verify Ethereum block headers, but the documentation explicitly states that this oracle can occasionally provide incorrect blockhashes.
Since the current implementation lacks sufficient safeguards against this scenario, this easily causes for incorrect blockhashes to be used, allowing for wrong price updates to be made
First note the documentation in regards to the blockhash-oracle
It can be updated frequently with a mainnet blockhash that is no older than, say, 30 minutes. The minimal delay is 64 blocks to avoid any potential mainnet reorg risks.
It can rarely provide an incorrect blockhash, but not an incorrect block number. Thus, a new update with a fresh block number will correct the parameters.
Also in the oracle itself, we hint this by allowing another update for the same block.nuber incase we do ingest an invalid blockhash, from both update_price
and update_profit_max_unlock_time
:
From the above, we can make a conclusion that on the average we would have a price active for ~ 30 minutes, also the documentation explicitly acknowledges that the blockhash oracle can occasionally return incorrect values, however the impact on this is wrongly concludede to just be the fact taht we would update the parameters in the next update which is invalid as the current implementation would mean that if we rely on the hash from this 100% of the time in some cases we would reject valid proofs and even worse could ingest invalid blocks.
To go into more details, let's examine the verification flow in ScrvusdVerifierV1.sol#L54-L68 :
As seen, the current implementation only verifies that the blockhash from the provided block header matches what's returned by the oracle. If the oracle provides an incorrect blockhash, we would have for ~ 30 minutes stale prices being ingested and even worse if the proof provided is malicious and tallies with the wrong blockhash from the oracle, we could easily set a wrong price on the destination chain, which could allow for one to extract value.
Crucially, the verifier doesn't perform any temporal validation to ensure blocks are processed in sequence, nor does it validate that the block timestamp is newer than previously processed blocks.
Now down in the oracle itself, the price is updated in as much as the block number is currently in the future:
Create new test_blockhash.py
test file in thetests/scrvusd/verifier/unitary/
directory and add the following:
Run with command:
Log output
When the blockhash oracle returns an incorrect value which is bound to happen, we have stale prices on the destination chain for ~ 30 minutes ( this would then be sorted out hopefully in the next update), however worse is the fact that fake proofs that are not pertaining to whats on the mainnet would be accepted by the verifier, which per the scope of the contest would mean we would end up with incorrect prices for scrvUSD on the destination chain and exploit path from here is enormous depending on if new price is way lower/higher than the real price on mainnet.
In short the current implementation not only promotes stale rates but even allows for where we would have divergent states on the destination chain and mainnet for scrvUSD params. whci is because though one could expect the provers to be trusted and only pass in valid data, cc:
Actors:
Prover: An off-chain prover (from now on, the prover) whose role is to fetch data from Ethereum that are useful for computing the growth rate of the vault, along with a proof that the data are valid.
Verifier: A smart contract that will be called by the prover to verify the data provided along with its proof.
There are no modifiers of any sort on the verification measures to ensure it's only being queried by the allowlisted provers.
NB: The exact same bug case is applicable to when we are attempting to update the
profit_max_unlock_time
in ScrvusdVerifierV2::verifyPeriodByBlockHash, as we could also allow for incorrect blockhashes returned by the oracle to be used.
Manual review
We could at the very least introduce more restrictive sequentiality in the price updates, which should validate that the new timestamp of the update is in the future (but within a valid range maybe within 6 hours of the previous update or something similar), this makes crafting the proof way more difficult and also makes the chances of this matching with the incorrect blockhash from the oracle much lower, since only having a block number in the future would not suffice, as is currently the case.
Generally though we should completely do away with a feature of the verifier if it provides incorrect data in some cases.
Even better imo, is to restrict the verification of data and then updating the price on the verifiers to said prover or set of provers, this can easily be done by introducing a new modifier which checks if the caller is among any of the allowlisted provers, notProver()
.
- Anything related to the output by the `BLOCK_HASH_ORACLE` is OOS per \[docs here]\(<https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle>). - The PoC utilizes a mock `BLOCK_HASH_ORACLE`which is not representative of the one used by the protocol - Even when block hash returned is incorrect, the assumption is already explicitly made known in the docs, and the contract allows a subsequent update within the same block to update and correct prices - All state roots and proofs must be verified by the OOS `StateProofVerifier` inherited as `Verifier`, so there is no proof that manipulating block timestamp/block number/inputs can affect a price update - There seems to be a lot of confusion on the block hash check. The block hash check is a unique identifier of a block and has nothing to do with the state root. All value verifications is performed by the OOS Verifier contract as mentioned above
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.