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

Potential Block Header Parsing Failures

Summary

Both the ScrvusdVerifierV1.sol and ScrvusdVerifierV2.sol contracts use the Verifier.parseBlockHeader() function to parse RLP-encoded block headers without sufficient error handling. If the RLP decoding fails internally, it may not be properly caught and handled by the contracts. This could lead to unexpected behavior, particularly when interacting with the oracle contract (ScrvusdOracleV2.vy), which relies on accurate block timestamps and numbers.

Vulnerability Details

Severity: Low

Files Affected:

  • contracts/scrvusd/verifiers/ScrvusdVerifierV1.sol

  • contracts/scrvusd/verifiers/ScrvusdVerifierV2.sol

Functions Affected:

  • verifyScrvusdByBlockHash()

  • verifyPeriodByBlockHash()

In both contracts, block headers are parsed without try/catch error handling:

// ScrvusdVerifierV1.sol - Line 55
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
// ScrvusdVerifierV2.sol - Line 24
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);

If the parseBlockHeader function encounters malformed RLP data or other parsing errors, it may revert with a generic error or produce unexpected results. The contracts do perform some basic validation after parsing:

require(block_header.hash != bytes32(0), "Invalid blockhash");

However, this only checks for a zero hash and doesn't fully validate the parsed block header structure or handle potential parsing exceptions.

The risk is elevated when considering how these values are used by the oracle contract (ScrvusdOracleV2.vy):

# From ScrvusdOracleV2.vy
@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
# ...
@external
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
access_control._check_role(UNLOCK_TIME_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
# ...

The oracle uses the block number to enforce a monotonically increasing sequence of updates, so parsing errors could disrupt this mechanism.

Impact

The lack of comprehensive error handling for block header parsing could lead to:

  1. Unpredictable Behavior: If parsing fails in unexpected ways, the contract might behave unpredictably, potentially using partially initialized block header data.

  2. Lack of Actionable Error Messages: When parsing fails, users would receive generic errors rather than specific, actionable information about what went wrong.

  3. Potential DoS Vector: In some scenarios, carefully crafted invalid block headers might cause parsing to fail in ways that consume significant gas without providing useful error feedback.

  4. Limited Debugging Information: Without specific error handling, diagnosing issues with block header parsing becomes more difficult.

  5. Block Sequence Disruption: If parsing issues cause incorrect block numbers to be sent to the oracle, it could disrupt the monotonicity checks in the oracle contract.

The overall impact is limited because:

  • RLP parsing failures typically result in transaction reversion

  • The StateProofVerifier library likely has internal validations

  • Subsequent validations like the blockhash check would catch some issues

  • The oracle's PRICE_PARAMETERS_VERIFIER and UNLOCK_TIME_VERIFIER roles would restrict who can call these functions (assuming appropriate access control is added)

Tools Used

  • Manual code review

Recommendations

  1. Add Input Validation Before Parsing:

    • Validate the block header RLP data before attempting to parse it:

function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
require(_block_header_rlp.length > 0, "Empty block header");
require(_block_header_rlp.length < 5000, "Block header too large"); // Reasonable size limit
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
// Rest of implementation
}
  1. Implement More Comprehensive Block Header Validation:

    • Add additional validation for the parsed block header, especially for fields used by the oracle:

function _validateBlockHeader(Verifier.BlockHeader memory header) internal pure {
require(header.hash != bytes32(0), "Invalid blockhash");
require(header.number > 0, "Invalid block number");
require(header.timestamp > 0, "Invalid timestamp");
require(header.timestamp < block.timestamp + 1 hours, "Future block timestamp");
require(header.stateRootHash != bytes32(0), "Invalid state root");
// Add other validations as needed
}
function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
_validateBlockHeader(block_header);
// Rest of implementation
}
  1. Consider Try/Catch for External Library Calls:

    • If the Solidity version supports it (0.6.0+), use try/catch for external calls:

function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
Verifier.BlockHeader memory block_header;
try Verifier.parseBlockHeader(_block_header_rlp) returns (Verifier.BlockHeader memory result) {
block_header = result;
} catch Error(string memory reason) {
revert(string(abi.encodePacked("Block header parsing failed: ", reason)));
} catch {
revert("Block header parsing failed with unknown error");
}
// Rest of implementation
}
  1. Add Logging for Parsing Attempts:

    • Emit events when parsing is attempted to aid in debugging:

event BlockHeaderParsingAttempted(uint256 dataLength);
event BlockHeaderParsingSucceeded(uint256 blockNumber, uint256 timestamp);
function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
emit BlockHeaderParsingAttempted(_block_header_rlp.length);
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
emit BlockHeaderParsingSucceeded(block_header.number, block_header.timestamp);
// Rest of implementation
}
  1. Align with Oracle's Block Number Requirements:

    • Ensure block number validation aligns with the oracle's expectations:

function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
// Oracle expects block numbers to be monotonically increasing
uint256 lastBlockNumber = IScrvusdOracle(SCRVUSD_ORACLE).last_block_number();
require(block_header.number >= lastBlockNumber, "Block number too old for oracle");
// Rest of implementation
}

By implementing these recommendations, particularly the input validation and more comprehensive block header validation, the contracts would be more robust against invalid or malicious block header inputs, providing clearer error messages and improving debugging capabilities.

Updates

Lead Judging Commences

0xnevi Lead Judge
6 months ago
0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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