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.