After the contracts' deployment, users can call the Verifiers functions to update the oracle's price or profit max unlock time with any valid past _block_number with its proof and set the scrvUSD price or profit_max_unlock_time to a value that is not accurate at the moment but it was in the past.
The update_price(...) and update_profit_max_unlock_time(...) functions in ScrvusdOracleV2.vy only check that the new _block_number is greater or equal to the last_block_number:
The last_block_number value doesn't get initialized anywhere, meaning that after contract deployment it is equal to 0.
That means that users can call verifyScrvusdByBlockHash(...) and verifyScrvusdByStateRoot(...) in the ScrvusdVerifierV1 contract or verifyPeriodByBlockHash(...) and verifyPeriodByStateRoot(...) in the ScrvusdVerifierV2 contract with a block number greater than 0 as long as the user has valid Merkle Patricia proofs for the scrvUSD vault's storage slots for that block.
Attackers can backrun the contracts deployment or frontrun the first price/period update and set the price/period values to values of the past.
Let's follow this scenario for an attack example:
The ScrvusdOracleV2 and verifiers are deployed
Attacker creates a stableswap-ng pool that uses this oracle for scrvUSD
The attacker calls verifyScrvusdByStateRoot or verifyScrvusdByBlockHash with their chosen block
Attacker contributes significant liquidity to that pool
If an attacker manipulates the oracle to report a LOW price for scrvUSD:
When adding liquidity, they can contribute LESS scrvUSD per unit of other tokens
The pool treats this smaller amount as "fair" based on the manipulated price
They essentially underpay in scrvUSD for their LP position
After some time the oracle will report a HIGHER/VALID price for scrvUSD and more LPs join in:
When removing liquidity, they receive MORE of the non-scrvUSD tokens
Since the pool thinks scrvUSD is worth more, it gives more of the other tokens to maintain "balance"
They essentially extract extra value from the pool
Manual review
Initialize the last_block_number as the current block.number.
- 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
I believe low to be appropriate, although could hear arguments for informational. The next `_block_number` for each price/max unlock time update will always be greater than the default zero, so the assertion of `assert self.last_block_number <= _block_number, "Outdated"` will pass without issue, but for consistency could be updated during deployment. Arguably at deployment, an update that has been verified via the verifier has not occur yet, so there would likely be no issues here given after the first correct update it will work as intended. The first update for price/profit max unlock time will also unlikely be outdated based on block number, which can be presumed to be true given this are extracted and verified within the OOS `StateProofVerifier`.
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.