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

State Root Verification Bypass in ScrvusdVerifierV1

Summary

  • Severity: HIGH

  • Category: Oracle Security / Data Validation

  • Impact: Possible manipulation of scrvUSD price oracle

  • Likelihood: Medium

Vulnerability Details

Description

In ScrvusdVerifierV1.sol, the verifyScrvusdByStateRoot function relies on the block number parameter to fetch the state root, but there's no verification that the provided _block_number corresponds to the block where the proof data was actually generated. This could allow an attacker to mix state proofs from one block with a different block number.

Affected Code

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);
// Use last_profit_update as the timestamp surrogate
return _updatePrice(params, params[5], _block_number);
}

Attack Scenario

  1. Attacker monitors scrvUSD contract state changes on Ethereum

  2. When favorable parameters exist in block N, attacker:

    • Captures the state proof from block N

    • Calls verifyScrvusdByStateRoot with a different block number M

    • If the state root for block M happens to validate against the proof from block N

    • The oracle receives incorrect historical data

Technical Impact

  1. Price Manipulation: Could allow injection of incorrect historical prices

  2. Stale Data: System could operate on outdated state information

  3. Oracle Reliability: Compromises the trustworthiness of the cross-chain price feed

Proof of Concept

def test_state_root_verification_bypass(setup_verifier):
verifier, block_oracle, admin = setup_verifier
# Get legitimate proof from block N
block_n = 1000
proof_data = generate_proof_data(block_n)
# Try submitting with different block numbers
for test_block in range(block_n - 100, block_n + 100):
try:
with boa.env.prank(admin):
# If this succeeds with wrong block number, vulnerability exists
verifier.verifyScrvusdByStateRoot(test_block, proof_data)
print(f"Verification succeeded with wrong block: {test_block}")
return True
except:
continue
return False

Mitigation

Add verification that the proof data matches the claimed block number:

function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
// Add block number validation
require(_validateProofBlockNumber(_proof_rlp, _block_number), "Proof-block mismatch");
uint256[PARAM_CNT] memory params = _extractParametersFromProof(state_root, _proof_rlp);
return _updatePrice(params, params[5], _block_number);
}
function _validateProofBlockNumber(bytes memory _proof_rlp, uint256 _block_number) internal pure returns (bool) {
// Extract block number from proof and validate
// Implementation depends on proof format
return true; // Placeholder
}

Alternative Mitigations

  1. Include block number in the proof data and validate it matches

  2. Add a timestamp freshness check

  3. Implement proof expiration mechanism

Severity Justification

This is a HIGH severity issue because:

  1. Impact: Could compromise the integrity of the oracle's price feed

  2. Real-world Exploitability: Practical in production environments

  3. Economic Risk: Could affect DeFi protocols relying on this oracle

  4. Recovery Difficulty: Challenging to detect and correct historical data

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.