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

Incorrect Computation of Storage Keys for Plain Variables

https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/verifiers/ScrvusdVerifierV1.sol#L102


In the _extractParametersFromProof function, the code extracts each storage value by computing the key as

keccak256(abi.encode(PARAM_SLOTS[i]))

for every parameter. This works correctly for the last parameter (“balanceOf(self)”) since that value is in a mapping (and the key for a mapping entry is computed as keccak256(abi.encode(key, slot))). However, for the other parameters (such as total_debt, total_idle, totalSupply, etc.), these are plain storage variables stored at fixed slots (e.g. 21, 22, 20, …).

For plain storage variables in Ethereum, the key in the account’s storage trie is not the hash of the slot index; it is simply the 32‑byte (left‑padded) representation of the slot number. For example, for a variable stored at slot 21 the correct key should be:

bytes32(uint256(21))

—not

keccak256(abi.encode(21))

Consequences:
Because the verifier uses the wrong key derivation:

  • A genuine state proof (for example, one obtained from an Ethereum archive node via eth_getProof) will include storage entries keyed by the plain slot (e.g. 0x000…15 for slot 21).

  • The verifier, however, will look for a key equal to keccak256(abi.encode(21)), which is entirely different.

  • As a result, the extracted value for each plain variable will either be incorrect (likely defaulting to zero) or the proof will fail the check (if the node isn’t found).

This means that unless the scrvUSD vault was implemented in an unconventional way (storing even plain variables under hashed keys), the verifier will not be able to extract the correct parameters from the proof. Worse, if an attacker can craft a proof for the hashed key instead of the actual storage key, they might be able to manipulate the values that eventually get passed to the price oracle.


Impact:

can lead either to a failure in updating the price (if the proof is rejected) or to potential exploitation if an attacker can supply a custom state proof for the hashed key.


Proof of Concept:

  1. Expected Behavior:
    For a plain storage variable (e.g. total_debt at slot 21), the standard is that its value is stored under the key

    bytes32(uint256(21))

    in the account’s storage trie.

  2. What the Code Does:
    The contract computes the key as

    keccak256(abi.encode(21))

    which yields a completely different 32‑byte value.

  3. Implications:

    • A correct state proof (from a compliant Ethereum node) will provide the storage entry under bytes32(21).

    • The verifier will then call:

      Verifier.extractSlotValueFromProof(keccak256(abi.encode(PARAM_SLOTS[i])), account.storageRoot, proofs[i].toList());

      and fail to find a matching key.

    • This mismatch means the verifier might either revert (failing the update) or—if not properly handled—allow an attacker to supply a specially crafted proof that “proves” a value for the wrong key.


Tools Used:

manual.


Recommendation:

  • For Plain Variables:
    Change the key derivation to use the proper padded representation of the slot. For example, replace

    keccak256(abi.encode(PARAM_SLOTS[i]))

    with

    bytes32(PARAM_SLOTS[i])

    for parameters that are plain (non-mapping) storage variables.

Updates

Lead Judging Commences

0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[invalid] finding-storage-key-compute-wrong

See primary comments in issue #23

Support

FAQs

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