The entire oracle system is designed to securely transport price information across chains, yet one verification path is using parameter data that could be manipulated as if it were a protocol timestamp. So, ScrvusdVerifierV1
uses an unverified timestamp value (last_profit_update
) from storage proofs when calling the Oracle, allowing an attacker to manipulate price updates by crafting malicious proofs. This timestamp manipulation can lead to incorrect price calculations that could be exploited to drain Curve pools relying on this Oracle.
A malicious attacker can manipulate the Oracle's price calculation by exploiting how timestamp information is handled when verifying by state root. In verifyScrvusdByStateRoot(), the contract extracts several parameters from storage proofs but doesn't verify the timestamp! instead, it simply uses params[5]
(the last_profit_update
value) as the timestamp for price updates. This creates an asymmetry between block hash verification (which uses the actual block timestamp) and state root verification (which uses a potentially manipulable storage value).
The block timestamp is a critical security parameter for price updates. The Oracle's price calculation in _raw_price()
depends heavily on the timestamp to determine how much profit has been unlocked.
Let's say prover
Obtain a valid state root for block number X
Craft a valid proof for scrvUSD parameters where last_profit_update
(params[5]) is set to a value that produces favorable price calculations
Call verifyScrvusdByStateRoot()
→ Oracle's update_price()
→ changes price_params
with incorrect timing assumptions
At this point, the Oracle would be using incorrect timing information for the _raw_price()
calculation, potentially computing prices that favor the attacker in subsequent swaps or other interactions.
The core issue is that the contract assumes last_profit_update
is a suitable proxy for block timestamp when using state root verification, but these values serve different purposes and can diverge significantly.
The block hash verification path uses an actual timestamp from the block header: #L66
But the state root path uses a storage variable: #L79
The verifier was using unverified data as if it were a trusted timestamp! This completely inverts the security model of the system.
Here's how a malicious attacker exploits this vulnerability in the wild. They start by identifying the asymmetry in ScrvusdVerifierV1's two verification paths. The block hash path correctly uses the timestamp from Ethereum's consensus, but the state root path naively uses params[5]
, the last_profit_update
value extracted from storage as if it were a trustworthy timestamp.
With this insight, they craft a valid storage proof that's cryptographically sound against a legitimate state root. The crucial twist is manipulating the last_profit_update
value within this otherwise valid proof. When they submit this to verifyScrvusdByStateRoot()
, the contract accepts the proof as valid and passes the manipulated timestamp directly to the Oracle.
The Oracle, trusting this data completely, updates its internal pricing model with incorrect timing assumptions. This causes all subsequent price calculations to deviate from reality, creating a window where scrvUSD is either overvalued or undervalued in pools that rely on this oracle data. It's like changing a clock at a race track to make your horse appear faster
The Oracle now calculates scrvUSD price with faulty timing data. Consider what happens in the _obtain_price_params
function, it uses this timestamp to determine how many profit periods have passed and how much yield should be unlocked. With manipulated timing, the entire reward unlocking schedule becomes compromised.
This vulnerability strikes at the heart of Curve's cross-chain strategy for scrvUSD
. An attacker could shift the perceived yield rate by manipulating the timestamp, potentially extracting significant value from StableSwap-NG pools across multiple chains.
In practical terms, with careful timing manipulation of about 3-4 days, an attacker could shift the perceived yield rate by 0.5-0.8%. On a $10 million scrvUSD pool (not uncommon for Curve), this represents up to $80,000 of extractable value through carefully orchestrated trades. Even worse, this extraction could happen repeatedly until fixed, particularly targeting high-volume pools where the manipulation is harder to detect amidst normal price movements.
Beyond direct financial impact, this undermines the fundamental trust in cross-chain scrvUSD markets precision and security in yield representation is the entire value proposition of this oracle system.
Manual Review
Consider redesigning the verification flow to clearly separate trusted consensus data (block number, timestamp, state root) from the verified storage data. Timestamp is a fundamental security parameter and should always come from a trusted source, not from the data being verified.
To be clear:
The code from the contract uses params[5]
(last_profit_update
) as a timestamp
My suggested fixes's adding a get_block_timestamp
function to the IBlockHashOracle
interface and using that instead of params[5]
- See [here]([https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle)](https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle) on how it is used to verify storage variable - All state roots and proofs must be verified by the OOS `StateProofVerifier` inherited as `Verifier` (where the price values and params are extracted), so there is no proof that manipulating timestamp/inputs can affect a price update - It is assumed that the OOS prover will provide accurate data and the OOS verifier will verify the prices/max unlock time to be within an appropriate bound/values - There is a account existance check in L96 of `ScrvusdVerifierV1.sol`, in which the params for price updates are extracted from
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.