DeFiLayer 1Layer 2
14,723 OP
View results
Submission Details
Severity: medium
Invalid

Block Number Replay Risk

Summary

The update_price function allows same-block updates to fix bad blockhashes, but this permits replaying parameters within the same block if the verifier allows it. This is partially mitigated by verifier trust but lacks additional safeguards.

Vulnerability Details

The vulnerable code is in update_price:

vyper

@external
def update_price(_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256) -> uint256:
assert self.last_block_number <= _block_number, "Outdated" # <= allows same block
...
  • Replay Window: Same _block_number can overwrite prior updates.

  • Intent: Docs note this is for fixing bad blockhashes.

PoC

Objective

Replay parameters in the same block to overwrite a legitimate update.

Prerequisites

  • PRICE_PARAMETERS_VERIFIER role.

  • Deployed scrvUSD oracle and a mock verifier allowing same-block updates.

Exploit Scenario

An attacker submits a legitimate update, then overwrites it with skewed parameters in the same block.

Proof of Concept Steps

  1. Setup: Deploy the oracle and verifier.

  2. Exploit Contract:

    solidity

    contract ExploitBlockReplay {
    IScrvusdOracle public oracle;
    constructor(address _oracle) {
    oracle = IScrvusdOracle(_oracle);
    }
    function exploit() external {
    uint256[7] memory legitParams = [1000, 500, 1000, block.timestamp + 7 days, 1000, block.timestamp - 1 days, 2000];
    uint256[7] memory badParams = [10000, 500, 1000, block.timestamp + 7 days, 1000, block.timestamp - 1 days, 2000];
    oracle.update_price(legitParams, block.timestamp, block.number); // Legit update
    oracle.update_price(badParams, block.timestamp, block.number); // Overwrite
    }
    }
  3. Execution:

    • Call exploit() in one transaction (same block).

    • Second call overwrites the first due to <= check.

  4. Result: Price reflects badParams, inflating scrvUSD value.

Outcome

Shows how same-block replays can manipulate pricing, relying on verifier weakness.

Impact

Parameter Overwrite: Malicious updates could override legitimate ones, mispricing scrvUSD.

  • Pool Risk: Temporary mispricing could be exploited before correction.

Tools Used

Manual Review

Recommendations

Require strictly increasing block numbers or add a nonce:

vyper

@external
def update_price(_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256) -> uint256:
assert self.last_block_number < _block_number, "Block number must increase" # < instead of <=
...
Updates

Lead Judging Commences

0xnevi Lead Judge 5 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

Support

FAQs

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