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

Unchecked Block Number Validation in verifyPeriodByStateRoot Enables Oracle Manipulation

Summary:
The verifyPeriodByStateRoot function in

ScrvusdVerifierV2.sol

lacks proper validation of the input block number, allowing attackers to use future or very old block numbers. This vulnerability could lead to manipulation of the oracle's price data and profit-unlocking schedules.

Vulnerability Details:
The vulnerable 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:

  1. Checking if it's a future block.

  2. Validating the age of the block.

  3. Ensuring minimum intervals between updates.

Adding Reproducibility Section:

// Test Contract
contract ScrvusdVerifierV2Exploit {
ScrvusdVerifierV2 private verifier;
function exploitFutureBlock() external {
// Attack Vector 1: Future Block
uint256 futureBlock = block.number + 1000;
bytes memory proofData = generateMockProof();
verifier.verifyPeriodByStateRoot(futureBlock, proofData);
// This succeeds when it should fail
}
function exploitStaleBlock() external {
// Attack Vector 2: Very Old Block
uint256 oldBlock = block.number - 100000;
bytes memory proofData = generateMockProof();
verifier.verifyPeriodByStateRoot(oldBlock, proofData);
// This succeeds with outdated state
}
}

Impact:

  • Attackers can manipulate profit_max_unlock_time by providing future block numbers

  • Use of outdated state roots could lead to incorrect price data

  • Potential premature profit unlocking affecting vault economics

  • Oracle manipulation could impact all dependent protocols and pools

  • Critical for price stability in stableswap-ng pools

Impact Validation Section:

def test_exploit_verification():
# Setup
block_number = chain.height
# 1. Future block attack
future_block = block_number + 1000
proof = generate_mock_proof()
result = verifier.verifyPeriodByStateRoot(future_block, proof)
assert result == True # Should fail but doesn't
# 2. Verify price manipulation
manipulated_price = oracle.price()
assert manipulated_price != original_price
# 3. Check profit unlocking schedule
unlocked_profit = vault.getUnlockedProfit()
assert unlocked_profit > expected_unlocked_profit

Scope Alignment:
The vulnerability directly impacts:

-ScrvusdOracleV2.vy - Main oracle contract
-Stableswap-ng pools using this oracle
-All contracts in contracts/scrvusd/ (explicitly in scope)

Attack Flow Diagram:

1. Attacker calls verifyPeriodByStateRoot()
└─> Provides future/stale block number
└─> Oracle accepts invalid state root
└─> Manipulated profit_max_unlock_time
└─> Affects:
├─> Price calculations
├─> Profit unlocking schedule
└─> Dependent stableswap-ng pools

This vulnerability is particularly severe because:

-It affects core oracle functionality
-Impacts multiple downstream systems
-No existing safeguards
-Easy to exploit
-Direct financial impact through price manipulation

Tools Used:
-Manual code review

Recommendations:

  1. Implement block number validation:

uint256 constant public MAX_BLOCK_DELAY = 256;
function verifyPeriodByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (bool) {
// Prevent future blocks
require(_block_number < block.number, "Future block");
// Prevent too old blocks
require(block.number - _block_number <= MAX_BLOCK_DELAY, "Block too old");
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);
}
  1. Add update frequency limits:

uint256 public lastVerifiedBlock;
uint256 constant MIN_BLOCK_INTERVAL = 4;
require(block.number - lastVerifiedBlock >= MIN_BLOCK_INTERVAL, "Too frequent");
lastVerifiedBlock = block.number;
  1. Implement event emission for better monitoring:

event StateRootVerified(
uint256 indexed blockNumber,
bytes32 stateRoot,
uint256 period
);

These changes will ensure the oracle maintains accurate and timely price data while preventing manipulation through block number attacks.

Updates

Lead Judging Commences

0xnevi Lead Judge 2 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope
Assigned finding tags:

[invalid] finding-missing-proof-content-validation

- See [here]([https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle)](https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle) on how it is used to verify storage variable - All state roots and proofs must be verified by the OOS `StateProofVerifier` inherited as `Verifier` (where the price values and params are extracted), so there is no proof that manipulating timestamp/inputs can affect a price update - It is assumed that the OOS prover will provide accurate data and the OOS verifier will verify the prices/max unlock time to be within an appropriate bound/values - There is a account existance check in L96 of `ScrvusdVerifierV1.sol`, in which the params for price updates are extracted from

Support

FAQs

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