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

Lack of Block Age Validation in ScrvusdVerifier State Proofs

Summary

The ScrvusdVerifierV1 and ScrvusdVerifierV2 contracts do not enforce a block finality check, leaving them susceptible to processing data from recently mined Ethereum blocks that may be reorganized.

Vulnerability Details

The ScrvusdVerifierV1 and ScrvusdVerifierV2 contracts do not include a block finality check in their state proof verification functions, such as ScrvusdVerifierV1::verifyScrvusdByBlockHash and ScrvusdVerifierV2::verifyPeriodByBlockHash:

function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
require(block_header.hash != bytes32(0), "Invalid blockhash");
require(
block_header.hash == IBlockHashOracle(BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
"Blockhash mismatch"
);
uint256[PARAM_CNT] memory params = _extractParametersFromProof(block_header.stateRootHash, _proof_rlp);
return _updatePrice(params, block_header.timestamp, block_header.number);
}

If a proof is submitted for a very recent block (e.g., one mined just one or two blocks ago), that block could be reorganized out of the chain. For example, if a competing chain overtakes the current one, the block hash or state root used in the proof might no longer be valid. This invalidation could lead the contract to accept or process data that becomes outdated or incorrect after the reorganization.

function verifyPeriodByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (bool) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
require(block_header.hash != bytes32(0), "Invalid blockhash");
require(
block_header.hash == IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
"Blockhash mismatch"
);
uint256 period = _extractPeriodFromProof(block_header.stateRootHash, _proof_rlp);
return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, block_header.number);
}

If the block used in the proof is reorganized, the profit_max_unlock_time or other extracted values might no longer be valid. For instance, a reorganization could alter the state root, changing the underlying data.

These functions process proofs tied to specific blocks without verifying that those blocks are old enough to be considered stable within the blockchain’s canonical chain.

Ethereum blocks, especially recent ones, can be subject to reorganizations due to network latency, miner competition, or forks. During a reorganization, a block initially accepted into the chain may be replaced, rendering its data invalid. Without a mechanism to ensure that the blocks used in proofs are sufficiently old (e.g., 15–30 blocks behind the current block), the contracts are vulnerable to accepting data from unstable blocks. This could result in the contracts updating their internal state with incorrect or outdated information.

Impact

The contracts may process data from blocks that are later reorganized, leading to invalid or outdated updates, such as incorrect price or period values.

Tools Used

Manual Review

Recommendations

To address this vulnerability, implement a block finality check in the affected functions:

  1. Define a Minimum Block Delay:

uint256 public constant MIN_BLOCK_DELAY = 15; // A value of 15–30 blocks is typically sufficient to ensure block stability.
  1. Implement the Check

require(
block.number > block_header.number &&
block.number - block_header.number >= MIN_BLOCK_DELAY,
"block too fresh"
);
Updates

Lead Judging Commences

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

Support

FAQs

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