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

Lack of State Root Validation

Summary

Both the ScrvusdVerifierV1.sol and ScrvusdVerifierV2.sol contracts retrieve state roots from an external block hash oracle but do not validate that these state roots are non-zero or otherwise valid before using them for proof verification. This could lead to processing invalid proofs if the state root is unavailable or incorrect, potentially affecting the accuracy of the price and period updates in the scrvUSD oracle system.

Vulnerability Details

Severity: Medium

Files Affected:

  • contracts/scrvusd/verifiers/ScrvusdVerifierV1.sol

  • contracts/scrvusd/verifiers/ScrvusdVerifierV2.sol

    Functions Affected:

  • verifyScrvusdByStateRoot()

  • verifyPeriodByStateRoot()

In both contracts, state roots are retrieved from the block hash oracle without validation:

// ScrvusdVerifierV1.sol - Line 76
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
// No validation that state_root != bytes32(0)
// ScrvusdVerifierV2.sol - Line 41
bytes32 state_root = IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_state_root(_block_number);
// No validation that state_root != bytes32(0)

The contracts then proceed to use these potentially invalid state roots for Merkle proof verification:

// ScrvusdVerifierV1.sol
uint256[PARAM_CNT] memory params = _extractParametersFromProof(state_root, _proof_rlp);
// ScrvusdVerifierV2.sol
uint256 period = _extractPeriodFromProof(state_root, _proof_rlp);

If the block hash oracle returns a zero state root (which can happen if the requested block is not available), the subsequent proof verification might behave unexpectedly, potentially allowing invalid proofs to be processed.

This is particularly concerning when examining the oracle contract (ScrvusdOracleV2.vy), which relies on accurate parameter updates for its price calculations:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
# Allowing same block updates for fixing bad blockhash provided (if possible)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
# ... updates critical price parameters ...
self.price_params = PriceParams(
total_debt=_parameters[0],
total_idle=_parameters[1],
total_supply=_parameters[2],
full_profit_unlock_date=_parameters[3],
profit_unlocking_rate=_parameters[4],
last_profit_update=_parameters[5],
balance_of_self=_parameters[6],
)

The oracle trusts that these parameters are correctly extracted from valid state proofs, but without proper state root validation, there's a risk of providing incorrect parameters.

Impact

The lack of state root validation could lead to several issues:

  1. Processing Invalid Proofs: If the state root is zero or invalid, the Merkle proof verification might still proceed but with incorrect results, potentially extracting invalid parameters.

  2. Parameter Manipulation: An attacker could potentially exploit this to manipulate the extracted parameters by providing carefully crafted proofs that work with a zero or invalid state root.

  3. Price Calculation Errors: Since the oracle uses these parameters for complex price calculations, invalid inputs could lead to incorrect price determinations:

    # From ScrvusdOracleV2.vy
    def _raw_price(ts: uint256, parameters_ts: uint256) -> uint256:
    parameters: PriceParams = self._obtain_price_params(parameters_ts)
    return self._total_assets(parameters) * 10**18 // self._total_supply(parameters, ts)
  4. Oracle Reliability Issues: The contract implicitly trusts the block hash oracle without verifying its output, which could lead to incorrect price or period updates if the oracle malfunctions.

  5. Potential DOS: If the state root retrieval consistently fails but doesn't revert, the contract might continue operating with incorrect data, potentially disrupting protocol operations.

Tools Used

  • Manual code review

Recommendations

  1. Add Explicit State Root Validation:

    • Add a requirement to check that the retrieved state root is non-zero.

// For ScrvusdVerifierV1.sol
function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
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);
// Rest of implementation
}
// For ScrvusdVerifierV2.sol
function verifyPeriodByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (bool) {
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
require(state_root != bytes32(0), "Invalid state root");
uint256 period = _extractPeriodFromProof(state_root, _proof_rlp);
// Rest of implementation
}
  1. Implement Block Recency Check:

    • Add validation to ensure the block number is within a reasonable range to prevent using outdated or far-future blocks, which aligns with the oracle's block number validation.

function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
require(_block_number < block.number, "Block number in the future");
require(block.number - _block_number <= 256, "Block too old");
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
require(state_root != bytes32(0), "Invalid state root");
// Rest of implementation
}
  1. Add Event for State Root Retrieval:

    • Emit an event when retrieving a state root to facilitate monitoring and debugging.

event StateRootRetrieved(uint256 blockNumber, bytes32 stateRoot);
function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
require(state_root != bytes32(0), "Invalid state root");
emit StateRootRetrieved(_block_number, state_root);
// Rest of implementation
}
  1. Parameter Validation:

    • Add validation for the extracted parameters before passing them to the oracle to ensure they're within reasonable bounds based on the oracle's expectations.

function _validateParameters(uint256[PARAM_CNT] memory params) internal pure {
// Ensure non-zero values for critical parameters
require(params[2] > 0, "Total supply cannot be zero"); // total_supply
// Add other validation based on parameter roles in the oracle
}
function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
// Get state root and extract parameters
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);
_validateParameters(params);
return _updatePrice(params, params[5], _block_number);
}

By implementing these recommendations, particularly the explicit state root validation and parameter validation, the contracts would be more robust against invalid or malicious inputs and less susceptible to potential exploitation through invalid state roots.

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.