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

Missing Block Recency Validation Enables Cross-Chain Profit Unlock Time Manipulation via Outdated Proofs

Description

The verifyPeriodByStateRoot function in the ScrvusdVerifierV2 contract lacks proper validation for the _block_number parameter. While the Oracle contract checks that the provided block number is not older than the most recently processed block, there is no validation to ensure the block number is reasonably recent relative to the current block.

In the verifyPeriodByStateRoot function:

function verifyPeriodByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (bool) {
bytes32 state_root = IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_state_root(_block_number);
uint256 period = _extractPeriodFromProof(state_root, _proof_rlp);
return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, _block_number);
}

The function accepts any block number without validating its recency. The Oracle's update_profit_max_unlock_time function only checks:

assert self.last_block_number <= _block_number, "Outdated"

This means an attacker could use a block that is only slightly newer than the last processed block but still significantly outdated, or even use future blocks.

Impact

This vulnerability could allow an attacker to manipulate the profit_max_unlock_time parameter by selectively choosing blocks with favorable parameter values. The impact is significant because the profit_max_unlock_time directly affects price calculation mechanics across the scrvUSD ecosystem. When outdated block data is used, the oracle computes incorrect unlock times for profits, leading to mispriced scrvUSD tokens across multiple blockchains.

This creates arbitrage opportunities where an attacker could exploit the price differentials between chains, potentially extracting value from liquidity pools through swap operations. According to the documentation, manipulation of this rate "can lead to the pool being drained from one side" as the price oracle no longer accurately reflects the true economic value of the underlying assets. The cross-chain implementation amplifies this vulnerability, as price inconsistencies propagate through various dependent protocols and stableswap pools that rely on accurate oracle data for pricing decisions.

Proof of Concept

  1. Wait for a significant change in the profit_max_unlock_time parameter on Ethereum.

  2. Submit a transaction to verifyPeriodByStateRoot using a block number that is newer than the last processed block but before the significant change.

  3. The Oracle will update its profit_max_unlock_time with the outdated value.

  4. Exploit price discrepancies in cross-chain stableswap pools that rely on this oracle for scrvUSD pricing.

Recommended Mitigation

Add validation checks to ensure the provided block number is within a reasonable range:

function verifyPeriodByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (bool) {
// Add validation to ensure block number is within acceptable range
require(_block_number >= block.number - MAX_BLOCK_AGE, "Block too old");
require(_block_number <= block.number, "Block in the future");
bytes32 state_root = IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_state_root(_block_number);
uint256 period = _extractPeriodFromProof(state_root, _proof_rlp);
return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, _block_number);
}

Define a MAX_BLOCK_AGE constant at the top of the contract:

// Maximum age (in blocks) of a block that can be used for verification
uint256 private constant MAX_BLOCK_AGE = 100; // Adjust based on requirements

Apply the same fix to the verifyPeriodByBlockHash function for consistency.

Updates

Lead Judging Commences

0xnevi Lead Judge about 2 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.