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

Missing State Root Age Validation

1. Summary

  • Severity: High

  • Category: Input Validation

  • Impact: Price manipulation through stale state roots

  • Likelihood: High - easily exploitable with minimal resources


2. 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);
// No validation of block number age
uint256[PARAM_CNT] memory params = _extractParametersFromProof(state_root, _proof_rlp);
return _updatePrice(params, params[5], _block_number);
}
  • Contract: ScrvusdVerifierV1

  • Functions: verifyScrvusdByStateRoot, verifyScrvusdByBlockHash

  • Lines: 74-84


3. Vulnerability Details

Root Cause

The contract violates a core requirement from the README: "can be updated frequently with a mainnet blockhash that is no older than, say, 30 minutes". There are no checks to ensure state roots or block hashes are recent.

Attack Scenario

  1. Attacker identifies historical block with favorable price parameters

  2. Submits proof from old block through verifyScrvusdByStateRoot

  3. Oracle updates with stale price data

  4. Attacker exploits price difference in StableSwap pools


4. Proof of Concept (PoC)

def test_stale_state_root_attack():
"""Test exploitation of stale state roots"""
# Deploy contracts
verifier = ScrvusdVerifierV1.deploy(MOCK_BLOCK_ORACLE, MOCK_SCRVUSD_ORACLE)
# Get old block number (1 day old)
old_block = chain.height - 7200 # Assuming 12s block time
# Create valid proof from old block
old_proof = generate_valid_proof(old_block)
# Should revert but succeeds
tx = verifier.verifyScrvusdByStateRoot(old_block, old_proof)
assert tx.status == 1

5. Recommended Fix

Proposed Solution

contract ScrvusdVerifierV1 {
uint256 public constant MAX_BLOCK_AGE = 900; // 30 minutes at 2s block time
function _validateBlockAge(uint256 blockNumber) internal view {
require(blockNumber <= block.number, "Future block");
require(
block.number - blockNumber <= MAX_BLOCK_AGE,
"Block too old"
);
}
function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
_validateBlockAge(_block_number);
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);
}
}

Alternative Mitigation Strategies

  • Implement exponential backoff for older blocks

  • Add minimum number of confirmations requirement

  • Cache recent valid block numbers


6. Severity Justification

  • Impact: High

    • Direct violation of core requirement

    • Enables price manipulation

    • Affects all dependent StableSwap pools

    • Could lead to significant financial losses

  • Likelihood: High

    • No specialized tools needed

    • Easy to find favorable historical blocks

    • Simple to execute with minimal resources

    • No complex preconditions required

Updates

Lead Judging Commences

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