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

Unrestricted Block Number Input in Verifier Enables Oracle Manipulation

Summary

In the verifyScrvusdByStateRoot function of ScrvusdVerifierV1.sol, the _block_number parameter is not validated. Because of how BlockHashOracle handles unknown blocks (returning a fallback hash instead of reverting), an attacker can submit arbitrarily large or non-existent block numbers. This can corrupt or freeze oracle updates, causing severe economic and operational risks for protocols relying on accurate scrvUSD price data.

Vulnerability Details

The ScrvusdVerifierV1.sol contract contains a critical vulnerability in the verifyScrvusdByStateRoot function. This function accepts an arbitrary _block_number parameter without any validation before passing it to the block hash oracle:

function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
uint256[PARAM_CNT] memory params = _extractParametersFromProof(state_root, _proof_rlp);
return _updatePrice(params, params[5], _block_number);
}

Root Cause

  1. Unvalidated _block_number: The contract never checks whether _block_number is valid, it could be far in the future or an otherwise non-existent block.

  2. Fallback Hash Mechanism: The BlockHashOracle returns a fallback_hash for any block it doesn’t explicitly track, rather than reverting.

  3. Craftable Proofs Against Fake Roots: With control over _block_number and knowledge of fallback_hash, an attacker can craft “valid” RLP proofs for maliciously tailored state roots.

Fake Price Parameters

  • The attacker calls verifyScrvusdByStateRoot with a block number that triggers the oracle to return fallback_hash.

  • They supply an RLP proof matching this fallback hash but containing malicious parameter values -- artificially high or low “total_debt”, “totalSupply”, etc.

  • Because the contract never checks _block_number, it accepts the manipulated state root and updates local parameters in _updatePrice.

  • Permanent DoS on Oracle Updates

    • After calling with a huge _block_number (e.g., 2502^{50}250), the oracle sets self.last_block_number to that extremely high value.

    • Any legitimate updates for actual chain blocks (far lower than 2502^{50}250) will revert with "Outdated" or a similar check in the oracle. This effectively freezes all future real updates until that distant block is reached—which might never happen.

Impact

Impact: High - An attacker can manipulate price data in the Curve USD oracle, leading to incorrect pricing of scrvUSD. This price manipulation affects all integrating protocols that rely on this data, potentially causing economic damage across the DeFi ecosystem. Additionally, by setting an extremely high block number, the attacker permanently blocks legitimate updates until that block height is reached.

Likelihood: Medium - The attack requires crafting valid proofs against an invalid state root, which demands technical expertise. However, the absence of any block number validation makes the attack path directly exploitable without dependencies on other conditions.

POC

The attack consists of two main steps:

Manipulate Price

uint256 farFutureBlock = 2**50; // extremely large
bytes memory maliciousProof = craftMaliciousProof(fallback_hash, {
// sets total_debt to 0, total_idle to huge, or other manipulations
});
verifier.verifyScrvusdByStateRoot(farFutureBlock, maliciousProof);
// => _updatePrice is called with bogus parameters
// => Price manipulated on cross-chain oracle

Block legitimate updates:

// The oracle sees the new last_block_number = 2**50.
// Future calls with legitimate block numbers < 2**50 revert as "Outdated".
// Real pricing updates cannot proceed until block #2**50, effectively indefinite lockout.

The key insight is the BlockHashOracleMock shows the expected behavior:

@view
@external
def get_state_root(_number: uint256) -> bytes32:
if self.state_root[_number] == empty(bytes32):
return self.fallback_hash
else:
return self.state_root[_number]


With an invalid block number, it returns fallback_hash rather than reverting, allowing an attacker to generate proofs that work against this known hash.

Recommendations

Implement proper validation of the _block_number parameter in the verifier contract. Add checks to ensure the block number is:

  1. Not greater than the current block number

  2. Within a reasonable range of the current block -- no more than 256 blocks in the past due to EVM limitations

  3. Corresponds to a block that actually exists on-chain

function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
// Add validation
require(_block_number <= block.number, "Block number in the future");
require(block.number - _block_number <= 256, "Block too old for validation");
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
require(state_root != bytes32(0), "Invalid state root");
uint256[PARAM_CNT] memory params = _extractParametersFromProof(state_root, _proof_rlp);
return _updatePrice(params, params[5], _block_number);
}

Additionally, modify the oracle to implement its own validation on block numbers, creating a defense-in-depth approach.

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

Appeal created

johnlaw Submitter
5 months ago
0xnevi Lead Judge
5 months ago
0xnevi Lead Judge 4 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.