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

Incorrect State Trie Key Hashing (`SCRVUSD_HASH`) Breaks Proof Verification in `ScrvusdVerifierV1`

Summary

ScrvusdVerifierV1.sol computes the account key hash (SCRVUSD_HASH) for the scrvUSD contract as keccak256(abi.encodePacked(SCRVUSD)), passing it to Verifier.extractAccountFromProof() to verify Ethereum state proofs. However, Ethereum’s state trie requires account keys to be hashed as keccak256(RLP.encode(address)). This mismatch causes proof verification to fail consistently, as the provided key does not correspond to any valid trie path. As a result, the contract cannot extract scrvUSD storage data, preventing oracle price updates to ScrvusdOracleV2. This critical flaw renders the verifier non-functional, leading to stale or default pricing in downstream systems, with a High Severity impact due to its direct effect on protocol functionality.

Vulnerability Details

The vulnerability arises in how ScrvusdVerifierV1 prepares the key (SCRVUSD_HASH) to verify the scrvUSD account state within Ethereum’s state trie:

  • Incorrect Hash Computation:

    28: address constant SCRVUSD = 0x0655977FEb2f289A4aB78af67BAB0d17aAb84367;
    @> 29: bytes32 constant SCRVUSD_HASH = keccak256(abi.encodePacked(SCRVUSD));
    • abi.encodePacked(SCRVUSD): Outputs the raw 20-byte address (e.g., 0x0655977FEb2f289A4aB78af67BAB0d17aAb84367).

    • keccak256(abi.encodePacked(SCRVUSD)): Hashes the 20 bytes directly, producing a bytes32 value (e.g., keccak256(0x0655977FEb2f289A4aB78af67BAB0d17aAb84367)).

  • Usage in Proof Verification:

    83: function _extractParametersFromProof(
    bytes32 stateRoot,
    bytes memory proofRlp
    ) internal view returns (uint256[PARAM_CNT] memory) {
    RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
    require(proofs.length == PROOF_CNT, "Invalid number of proofs");
    Verifier.Account memory account = Verifier.extractAccountFromProof(
    SCRVUSD_HASH,
    stateRoot,
    proofs[0].toList()
    );
    require(account.exists, "scrvUSD account does not exist");
    uint256[PARAM_CNT] memory params;
    for (uint256 i = 1; i < PROOF_CNT; i++) {
    Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
    keccak256(abi.encode(PARAM_SLOTS[i])),
    account.storageRoot,
    proofs[i].toList()
    );
    params[i - 1] = slot.value;
    }
    return params;
    }
    • SCRVUSD_HASH is passed as the key to extractAccountFromProof(), which expects the trie key for SCRVUSD’s account data.

  • Ethereum State Trie Expectation:

    • In Ethereum’s Patricia Merkle Trie, account keys are computed as keccak256(RLP.encode(address)).

    • RLP encoding of a 20-byte address prepends a length prefix (0xd4) to the address bytes (e.g., 0xd40655977feb2f289a4ab78af67bab0d17aab84367).

    • Correct key: keccak256(0xd40655977feb2f289a4ab78af67bab0d17aab84367) - differs from the raw hash.

  • Mismatch:

    • Provided: keccak256(raw 20 bytes) - doesn’t match any trie node.

    • Expected: keccak256(RLP-encoded address) - the actual path to SCRVUSD’s account in the state trie.

    • Result: extractAccountFromProof() fails to find the account, returning account.exists = false, triggering a revert at require(account.exists).

  • Affected Functions:

    • verifyScrvusdByBlockHash() and verifyScrvusdByStateRoot() call _extractParametersFromProof(), both failing due to this error.

Impact

  • Every verification attempt reverts at require(account.exists, "scrvUSD account does not exist"), preventing _extractParametersFromProof() from completing.

  • No parameters are extracted, so _updatePrice() is never called on ScrvusdOracleV2.

  • If never updated: ScrvusdOracleV2 remains at its initial state, misrepresenting scrvUSD’s true vault value.

  • If previously updated: Stale data persists, lagging behind Ethereum’s scrvUSD state.

Tools Used

  • Manual Code Review

  • Ethereum Documentation

Recommendations

  • Use RLP encoding to match the state trie key:

    bytes32 constant SCRVUSD_HASH = keccak256(
    abi.encodePacked(uint8(0xd4), SCRVUSD) // RLP: 0xd4 (20-byte prefix) + address
    );
Updates

Lead Judging Commences

0xnevi Lead Judge
5 months ago
0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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