DeFiLayer 1Layer 2
14,723 OP
View results
Submission Details
Severity: low
Valid

Initially attackers can pass any block number's proof as legit and report a past price

Summary

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.

Vulnerability Details

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:

def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
..
# Allowing same block updates for fixing bad blockhash provided (if possible)
@> assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
..
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
..
# Allowing same block updates for fixing bad blockhash provided (if possible)
@> assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _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.

Impact

Attackers can backrun the contracts deployment or frontrun the first price/period update and set the price/period values to values of the past.

POC

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

    1. 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

    2. 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

Tools Used

Manual review

Recommendations

Initialize the last_block_number as the current block.number.

Updates

Lead Judging Commences

0xnevi Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope
Assigned finding tags:

[invalid] finding-block-number-no-input-check

- 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

Appeal created

kalogerone Submitter
3 months ago
0xnevi Lead Judge
2 months ago
0xnevi Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-block-number-not-initialized

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`.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.